Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a07e34fb4 | |||
| 1b5d614f5a | |||
| abb08260eb | |||
| 57c567c570 | |||
| 424f4601ae | |||
| a2a247b413 | |||
| 0b6a369047 | |||
| 522fdefb8c | |||
| 4d8d10f07a | |||
| 6f17129364 | |||
| 6f44e4fd09 | |||
| 02ddf80014 | |||
| 723d9eddd1 | |||
| fa655b812c | |||
| 576c10cd50 | |||
| 179afe7e0d | |||
| be7f42c454 | |||
| 7e1ee26dee | |||
| 1c9b449c46 | |||
| e38095b727 | |||
| b5796ad237 | |||
| f985c437b5 | |||
| 63fb8f5933 | |||
| eb529783c4 | |||
| a8265db5f0 | |||
| 7dcf2b1abb | |||
| 514452ddb9 | |||
| 1eb55a83c0 | |||
| dfe6f2029a | |||
| 851c844c8b | |||
| 7e413ed406 | |||
| c56b8863ee | |||
| 13a8548d3a | |||
| a70cb51559 | |||
| 8a56b3db16 | |||
| 4fcf2ce7bd | |||
| 080ace90b2 | |||
| 2bfa81c707 |
+2
-2
@@ -18,5 +18,5 @@ using System.Resources;
|
||||
[assembly: AssemblyVersion("8.0.0")]
|
||||
|
||||
// these are FYI and changed automatically
|
||||
[assembly: AssemblyFileVersion("8.0.1")]
|
||||
[assembly: AssemblyInformationalVersion("8.0.1")]
|
||||
[assembly: AssemblyFileVersion("8.0.2")]
|
||||
[assembly: AssemblyInformationalVersion("8.0.2")]
|
||||
|
||||
@@ -70,7 +70,23 @@ namespace Umbraco.Core.Composing
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Type> PrepareComposerTypes()
|
||||
internal IEnumerable<Type> PrepareComposerTypes()
|
||||
{
|
||||
var requirements = GetRequirements();
|
||||
|
||||
// only for debugging, this is verbose
|
||||
//_logger.Debug<Composers>(GetComposersReport(requirements));
|
||||
|
||||
var sortedComposerTypes = SortComposers(requirements);
|
||||
|
||||
// bit verbose but should help for troubleshooting
|
||||
//var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine;
|
||||
_logger.Debug<Composers>("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes);
|
||||
|
||||
return sortedComposerTypes;
|
||||
}
|
||||
|
||||
internal Dictionary<Type, List<Type>> GetRequirements(bool throwOnMissing = true)
|
||||
{
|
||||
// create a list, remove those that cannot be enabled due to runtime level
|
||||
var composerTypeList = _composerTypes
|
||||
@@ -89,25 +105,69 @@ namespace Umbraco.Core.Composing
|
||||
// enable or disable composers
|
||||
EnableDisableComposers(composerTypeList);
|
||||
|
||||
// sort the composers according to their dependencies
|
||||
var requirements = new Dictionary<Type, List<Type>>();
|
||||
foreach (var type in composerTypeList) requirements[type] = null;
|
||||
foreach (var type in composerTypeList)
|
||||
void GatherInterfaces<TAttribute>(Type type, Func<TAttribute, Type> getTypeInAttribute, HashSet<Type> iset, List<Type> set2)
|
||||
where TAttribute : Attribute
|
||||
{
|
||||
GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements);
|
||||
GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements);
|
||||
foreach (var attribute in type.GetCustomAttributes<TAttribute>())
|
||||
{
|
||||
var typeInAttribute = getTypeInAttribute(attribute);
|
||||
if (typeInAttribute != null && // if the attribute references a type ...
|
||||
typeInAttribute.IsInterface && // ... which is an interface ...
|
||||
typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ...
|
||||
!iset.Contains(typeInAttribute)) // ... which is not already in the list
|
||||
{
|
||||
// add it to the new list
|
||||
iset.Add(typeInAttribute);
|
||||
set2.Add(typeInAttribute);
|
||||
|
||||
// add all its interfaces implementing IComposer
|
||||
foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x)))
|
||||
{
|
||||
iset.Add(i);
|
||||
set2.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only for debugging, this is verbose
|
||||
//_logger.Debug<Composers>(GetComposersReport(requirements));
|
||||
// gather interfaces too
|
||||
var interfaces = new HashSet<Type>(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y))));
|
||||
composerTypeList.AddRange(interfaces);
|
||||
var list1 = composerTypeList;
|
||||
while (list1.Count > 0)
|
||||
{
|
||||
var list2 = new List<Type>();
|
||||
foreach (var t in list1)
|
||||
{
|
||||
GatherInterfaces<ComposeAfterAttribute>(t, a => a.RequiredType, interfaces, list2);
|
||||
GatherInterfaces<ComposeBeforeAttribute>(t, a => a.RequiringType, interfaces, list2);
|
||||
}
|
||||
composerTypeList.AddRange(list2);
|
||||
list1 = list2;
|
||||
}
|
||||
|
||||
// sort the composers according to their dependencies
|
||||
var requirements = new Dictionary<Type, List<Type>>();
|
||||
foreach (var type in composerTypeList)
|
||||
requirements[type] = null;
|
||||
foreach (var type in composerTypeList)
|
||||
{
|
||||
GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing);
|
||||
GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements);
|
||||
}
|
||||
|
||||
return requirements;
|
||||
}
|
||||
|
||||
internal IEnumerable<Type> SortComposers(Dictionary<Type, List<Type>> requirements)
|
||||
{
|
||||
// sort composers
|
||||
var graph = new TopoGraph<Type, KeyValuePair<Type, List<Type>>>(kvp => kvp.Key, kvp => kvp.Value);
|
||||
graph.AddItems(requirements);
|
||||
List<Type> sortedComposerTypes;
|
||||
try
|
||||
{
|
||||
sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList();
|
||||
sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -117,40 +177,37 @@ namespace Umbraco.Core.Composing
|
||||
throw;
|
||||
}
|
||||
|
||||
// bit verbose but should help for troubleshooting
|
||||
//var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine;
|
||||
_logger.Debug<Composers>("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes);
|
||||
|
||||
return sortedComposerTypes;
|
||||
}
|
||||
|
||||
private static string GetComposersReport(Dictionary<Type, List<Type>> requirements)
|
||||
internal static string GetComposersReport(Dictionary<Type, List<Type>> requirements)
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
text.AppendLine("Composers & Dependencies:");
|
||||
text.AppendLine(" < compose before");
|
||||
text.AppendLine(" > compose after");
|
||||
text.AppendLine(" : implements");
|
||||
text.AppendLine(" = depends");
|
||||
text.AppendLine();
|
||||
|
||||
bool HasReq(IEnumerable<Type> types, Type type)
|
||||
=> types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface);
|
||||
|
||||
foreach (var kvp in requirements)
|
||||
{
|
||||
var type = kvp.Key;
|
||||
|
||||
text.AppendLine(type.FullName);
|
||||
foreach (var attribute in type.GetCustomAttributes<ComposeAfterAttribute>())
|
||||
text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
|
||||
? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
|
||||
: ""));
|
||||
foreach (var attribute in type.GetCustomAttributes<ComposeBeforeAttribute>())
|
||||
text.AppendLine(" -< " + attribute.RequiringType);
|
||||
foreach (var i in type.GetInterfaces())
|
||||
{
|
||||
text.AppendLine(" : " + i.FullName);
|
||||
foreach (var attribute in i.GetCustomAttributes<ComposeAfterAttribute>())
|
||||
text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
|
||||
? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
|
||||
: ""));
|
||||
foreach (var attribute in i.GetCustomAttributes<ComposeBeforeAttribute>())
|
||||
text.AppendLine(" -< " + attribute.RequiringType);
|
||||
var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true);
|
||||
text.AppendLine(" > " + attribute.RequiredType +
|
||||
(weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")");
|
||||
}
|
||||
foreach (var attribute in type.GetCustomAttributes<ComposeBeforeAttribute>())
|
||||
text.AppendLine(" < " + attribute.RequiringType);
|
||||
foreach (var i in type.GetInterfaces())
|
||||
text.AppendLine(" : " + i.FullName);
|
||||
if (kvp.Value != null)
|
||||
foreach (var t in kvp.Value)
|
||||
text.AppendLine(" = " + t);
|
||||
@@ -221,16 +278,16 @@ namespace Umbraco.Core.Composing
|
||||
types.Remove(kvp.Key);
|
||||
}
|
||||
|
||||
private static void GatherRequirementsFromRequireAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements)
|
||||
private static void GatherRequirementsFromAfterAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements, bool throwOnMissing = true)
|
||||
{
|
||||
// get 'require' attributes
|
||||
// these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
|
||||
var requireAttributes = type
|
||||
var afterAttributes = type
|
||||
.GetInterfaces().SelectMany(x => x.GetCustomAttributes<ComposeAfterAttribute>()) // those marking interfaces
|
||||
.Concat(type.GetCustomAttributes<ComposeAfterAttribute>()); // those marking the composer
|
||||
|
||||
// what happens in case of conflicting attributes (different strong/weak for same type) is not specified.
|
||||
foreach (var attr in requireAttributes)
|
||||
foreach (var attr in afterAttributes)
|
||||
{
|
||||
if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below)
|
||||
|
||||
@@ -238,13 +295,13 @@ namespace Umbraco.Core.Composing
|
||||
// unless strong, and then require at least one enabled composer implementing that interface
|
||||
if (attr.RequiredType.IsInterface)
|
||||
{
|
||||
var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList();
|
||||
var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList();
|
||||
if (implems.Count > 0)
|
||||
{
|
||||
if (requirements[type] == null) requirements[type] = new List<Type>();
|
||||
requirements[type].AddRange(implems);
|
||||
}
|
||||
else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak
|
||||
else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak
|
||||
throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
|
||||
}
|
||||
// requiring a class = require that the composer is enabled
|
||||
@@ -256,28 +313,28 @@ namespace Umbraco.Core.Composing
|
||||
if (requirements[type] == null) requirements[type] = new List<Type>();
|
||||
requirements[type].Add(attr.RequiredType);
|
||||
}
|
||||
else if (attr.Weak != true) // if not explicitly set to weak, is strong
|
||||
else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong
|
||||
throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements)
|
||||
private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements)
|
||||
{
|
||||
// get 'required' attributes
|
||||
// these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
|
||||
var requiredAttributes = type
|
||||
var beforeAttributes = type
|
||||
.GetInterfaces().SelectMany(x => x.GetCustomAttributes<ComposeBeforeAttribute>()) // those marking interfaces
|
||||
.Concat(type.GetCustomAttributes<ComposeBeforeAttribute>()); // those marking the composer
|
||||
|
||||
foreach (var attr in requiredAttributes)
|
||||
foreach (var attr in beforeAttributes)
|
||||
{
|
||||
if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below)
|
||||
|
||||
// required by an interface = by any enabled composer implementing this that interface
|
||||
if (attr.RequiringType.IsInterface)
|
||||
{
|
||||
var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList();
|
||||
var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList();
|
||||
foreach (var implem in implems)
|
||||
{
|
||||
if (requirements[implem] == null) requirements[implem] = new List<Type>();
|
||||
|
||||
@@ -203,6 +203,28 @@ namespace Umbraco.Core
|
||||
composition.RegisterUnique(_ => registrar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the database server messenger options.
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <param name="factory">A function creating the options.</param>
|
||||
/// <remarks>Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default.</remarks>
|
||||
public static void SetDatabaseServerMessengerOptions(this Composition composition, Func<IFactory, DatabaseServerMessengerOptions> factory)
|
||||
{
|
||||
composition.RegisterUnique(factory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the database server messenger options.
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <param name="options">Options.</param>
|
||||
/// <remarks>Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default.</remarks>
|
||||
public static void SetDatabaseServerMessengerOptions(this Composition composition, DatabaseServerMessengerOptions options)
|
||||
{
|
||||
composition.RegisterUnique(_ => options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the short string helper.
|
||||
/// </summary>
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Web;
|
||||
using System.Xml.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NPoco.Expressions;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Models;
|
||||
@@ -52,8 +53,8 @@ namespace Umbraco.Core
|
||||
return ContentStatus.Unpublished;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -134,9 +135,14 @@ namespace Umbraco.Core
|
||||
/// <summary>
|
||||
/// Sets the posted file value of a property.
|
||||
/// </summary>
|
||||
/// <remarks>This really is for FileUpload fields only, and should be obsoleted. For anything else,
|
||||
/// you need to store the file by yourself using Store and then figure out
|
||||
/// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself.</remarks>
|
||||
public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, HttpPostedFileBase postedFile, string culture = null, string segment = null)
|
||||
{
|
||||
content.SetValue(contentTypeBaseServiceProvider, propertyTypeAlias, postedFile.FileName, postedFile.InputStream, culture, segment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the posted file value of a property.
|
||||
/// </summary>
|
||||
public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null)
|
||||
{
|
||||
if (filename == null || filestream == null) return;
|
||||
|
||||
@@ -10,6 +10,23 @@ namespace Umbraco.Core
|
||||
///</summary>
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
internal static bool HasDuplicates<T>(this IEnumerable<T> items, bool includeNull)
|
||||
{
|
||||
var hs = new HashSet<T>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item != null || includeNull)
|
||||
{
|
||||
if (!hs.Add(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Wraps this object instance into an IEnumerable{T} consisting of a single item.
|
||||
/// </summary>
|
||||
|
||||
@@ -226,6 +226,8 @@ namespace Umbraco.Core.Migrations.Install
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
|
||||
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,21 @@ namespace Umbraco.Core.Models
|
||||
|
||||
protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
//enable this to detect duplicate property aliases. We do want this, however making this change in a
|
||||
//patch release might be a little dangerous
|
||||
|
||||
////detect if there are any duplicate aliases - this cannot be allowed
|
||||
//if (e.Action == NotifyCollectionChangedAction.Add
|
||||
// || e.Action == NotifyCollectionChangedAction.Replace)
|
||||
//{
|
||||
// var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias);
|
||||
// if (allAliases.HasDuplicates(false))
|
||||
// {
|
||||
// var newAliases = string.Join(", ", e.NewItems.Cast<PropertyType>().Select(x => x.Alias));
|
||||
// throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}");
|
||||
// }
|
||||
//}
|
||||
|
||||
OnPropertyChanged(nameof(PropertyTypes));
|
||||
}
|
||||
|
||||
@@ -397,15 +412,16 @@ namespace Umbraco.Core.Models
|
||||
var group = PropertyGroups[propertyGroupName];
|
||||
if (group == null) return;
|
||||
|
||||
// re-assign the group's properties to no group
|
||||
// first remove the group
|
||||
PropertyGroups.RemoveItem(propertyGroupName);
|
||||
|
||||
// Then re-assign the group's properties to no group
|
||||
foreach (var property in group.PropertyTypes)
|
||||
{
|
||||
property.PropertyGroupId = null;
|
||||
_noGroupPropertyTypes.Add(property);
|
||||
}
|
||||
|
||||
// actually remove the group
|
||||
PropertyGroups.RemoveItem(propertyGroupName);
|
||||
OnPropertyChanged(nameof(PropertyGroups));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Umbraco.Core.Models
|
||||
public class PropertyCollection : KeyedCollection<string, Property>, INotifyCollectionChanged, IDeepCloneable
|
||||
{
|
||||
private readonly object _addLocker = new object();
|
||||
internal Action OnAdd;
|
||||
|
||||
internal Func<Property, bool> AdditionValidator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -49,10 +49,12 @@ namespace Umbraco.Core.Models
|
||||
/// </summary>
|
||||
internal void Reset(IEnumerable<Property> properties)
|
||||
{
|
||||
//collection events will be raised in each of these calls
|
||||
Clear();
|
||||
|
||||
//collection events will be raised in each of these calls
|
||||
foreach (var property in properties)
|
||||
Add(property);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,8 +62,9 @@ namespace Umbraco.Core.Models
|
||||
/// </summary>
|
||||
protected override void SetItem(int index, Property property)
|
||||
{
|
||||
var oldItem = index >= 0 ? this[index] : property;
|
||||
base.SetItem(index, property);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property, index));
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,10 +123,8 @@ namespace Umbraco.Core.Models
|
||||
}
|
||||
}
|
||||
|
||||
//collection events will be raised in InsertItem with Add
|
||||
base.Add(property);
|
||||
|
||||
OnAdd?.Invoke();
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,6 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();
|
||||
|
||||
// TODO: this doesn't seem to be used anywhere
|
||||
internal Action OnAdd;
|
||||
|
||||
internal PropertyGroupCollection()
|
||||
{ }
|
||||
|
||||
@@ -37,16 +34,19 @@ namespace Umbraco.Core.Models
|
||||
/// <remarks></remarks>
|
||||
internal void Reset(IEnumerable<PropertyGroup> groups)
|
||||
{
|
||||
//collection events will be raised in each of these calls
|
||||
Clear();
|
||||
|
||||
//collection events will be raised in each of these calls
|
||||
foreach (var group in groups)
|
||||
Add(group);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
protected override void SetItem(int index, PropertyGroup item)
|
||||
{
|
||||
var oldItem = index >= 0 ? this[index] : item;
|
||||
base.SetItem(index, item);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem));
|
||||
}
|
||||
|
||||
protected override void RemoveItem(int index)
|
||||
@@ -84,6 +84,7 @@ namespace Umbraco.Core.Models
|
||||
if (keyExists)
|
||||
throw new Exception($"Naming conflict: Changing the name of PropertyGroup '{item.Name}' would result in duplicates");
|
||||
|
||||
//collection events will be raised in SetItem
|
||||
SetItem(IndexOfKey(item.Id), item);
|
||||
return;
|
||||
}
|
||||
@@ -96,16 +97,14 @@ namespace Umbraco.Core.Models
|
||||
var exists = Contains(key);
|
||||
if (exists)
|
||||
{
|
||||
//collection events will be raised in SetItem
|
||||
SetItem(IndexOfKey(key), item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//collection events will be raised in InsertItem
|
||||
base.Add(item);
|
||||
OnAdd?.Invoke();
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -19,9 +19,6 @@ namespace Umbraco.Core.Models
|
||||
[IgnoreDataMember]
|
||||
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();
|
||||
|
||||
// TODO: This doesn't seem to be used
|
||||
[IgnoreDataMember]
|
||||
internal Action OnAdd;
|
||||
|
||||
internal PropertyTypeCollection(bool supportsPublishing)
|
||||
{
|
||||
@@ -43,36 +40,44 @@ namespace Umbraco.Core.Models
|
||||
/// <remarks></remarks>
|
||||
internal void Reset(IEnumerable<PropertyType> properties)
|
||||
{
|
||||
//collection events will be raised in each of these calls
|
||||
Clear();
|
||||
|
||||
//collection events will be raised in each of these calls
|
||||
foreach (var property in properties)
|
||||
Add(property);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
Add(property);
|
||||
}
|
||||
|
||||
protected override void SetItem(int index, PropertyType item)
|
||||
{
|
||||
item.SupportsPublishing = SupportsPublishing;
|
||||
base.SetItem(index, item);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
|
||||
var oldItem = index >= 0 ? this[index] : item;
|
||||
base.SetItem(index, item);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem));
|
||||
item.PropertyChanged += Item_PropertyChanged;
|
||||
}
|
||||
|
||||
protected override void RemoveItem(int index)
|
||||
{
|
||||
var removed = this[index];
|
||||
base.RemoveItem(index);
|
||||
removed.PropertyChanged -= Item_PropertyChanged;
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
|
||||
}
|
||||
|
||||
protected override void InsertItem(int index, PropertyType item)
|
||||
{
|
||||
item.SupportsPublishing = SupportsPublishing;
|
||||
base.InsertItem(index, item);
|
||||
base.InsertItem(index, item);
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
|
||||
item.PropertyChanged += Item_PropertyChanged;
|
||||
}
|
||||
|
||||
protected override void ClearItems()
|
||||
{
|
||||
base.ClearItems();
|
||||
foreach (var item in this)
|
||||
item.PropertyChanged -= Item_PropertyChanged;
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
@@ -91,6 +96,7 @@ namespace Umbraco.Core.Models
|
||||
var exists = Contains(key);
|
||||
if (exists)
|
||||
{
|
||||
//collection events will be raised in SetItem
|
||||
SetItem(IndexOfKey(key), item);
|
||||
return;
|
||||
}
|
||||
@@ -103,10 +109,8 @@ namespace Umbraco.Core.Models
|
||||
item.SortOrder = this.Max(x => x.SortOrder) + 1;
|
||||
}
|
||||
|
||||
//collection events will be raised in InsertItem
|
||||
base.Add(item);
|
||||
OnAdd?.Invoke();
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -115,6 +119,17 @@ namespace Umbraco.Core.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a property changes on a PropertyType that exists in this collection
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var propType = (PropertyType)sender;
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this collection contains a <see cref="Property"/> whose alias matches the specified PropertyType.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,11 +9,10 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
{
|
||||
internal static class MemberTypeReadOnlyFactory
|
||||
{
|
||||
public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto, out bool needsSaving)
|
||||
public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto)
|
||||
{
|
||||
var standardPropertyTypes = Constants.Conventions.Member.GetStandardPropertyTypeStubs();
|
||||
needsSaving = false;
|
||||
|
||||
|
||||
var memberType = new MemberType(dto.ParentId);
|
||||
|
||||
try
|
||||
@@ -41,27 +40,29 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
var propertyTypeGroupCollection = GetPropertyTypeGroupCollection(dto, memberType, standardPropertyTypes);
|
||||
memberType.PropertyGroups = propertyTypeGroupCollection;
|
||||
|
||||
var propertyTypes = GetPropertyTypes(dto, memberType, standardPropertyTypes);
|
||||
memberType.NoGroupPropertyTypes = GetNoGroupPropertyTypes(dto, memberType, standardPropertyTypes);
|
||||
|
||||
//By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before.
|
||||
// By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before.
|
||||
// In theory this should not happen! The only reason this did happen was because:
|
||||
// A) we didn't install all of the default membership properties by default
|
||||
// B) the existing data is super old when we didn't store membership properties at all (very very old)
|
||||
// So what to do? We absolutely do not want to update the database when only a read was requested, this will cause problems
|
||||
// so we should just add these virtual properties, they will have no ids but they will have aliases and this should be perfectly
|
||||
// fine for any membership provider logic to work since neither membership providers or asp.net identity care about whether a property
|
||||
// has an ID or not.
|
||||
// When the member type is saved, all the properties will correctly be added.
|
||||
|
||||
//This will add this group if it doesn't exist, no need to error check here
|
||||
memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName);
|
||||
foreach (var standardPropertyType in standardPropertyTypes)
|
||||
{
|
||||
if (dto.PropertyTypes.Any(x => x.Alias.Equals(standardPropertyType.Key))) continue;
|
||||
//This will add the property if it doesn't exist, no need to error check here
|
||||
memberType.AddPropertyType(standardPropertyType.Value, Constants.Conventions.Member.StandardPropertiesGroupName);
|
||||
|
||||
// beware!
|
||||
// means that we can return a memberType "from database" that has some property types
|
||||
// that do *not* come from the database and therefore are incomplete eg have no key,
|
||||
// no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation
|
||||
needsSaving = true;
|
||||
|
||||
//Add the standard PropertyType to the current list
|
||||
propertyTypes.Add(standardPropertyType.Value);
|
||||
|
||||
//Internal dictionary for adding "MemberCanEdit", "VisibleOnProfile", "IsSensitive" properties to each PropertyType
|
||||
memberType.MemberTypePropertyTypes.Add(standardPropertyType.Key,
|
||||
new MemberTypePropertyProfileAccess(false, false, false));
|
||||
if (!memberType.MemberTypePropertyTypes.TryGetValue(standardPropertyType.Key, out var memberTypePropertyProfile))
|
||||
memberType.MemberTypePropertyTypes[standardPropertyType.Key] = new MemberTypePropertyProfileAccess(false, false, false);
|
||||
}
|
||||
memberType.NoGroupPropertyTypes = propertyTypes;
|
||||
|
||||
return memberType;
|
||||
}
|
||||
@@ -147,13 +148,14 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
return propertyGroups;
|
||||
}
|
||||
|
||||
private static List<PropertyType> GetPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary<string, PropertyType> standardProps)
|
||||
private static List<PropertyType> GetNoGroupPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary<string, PropertyType> standardProps)
|
||||
{
|
||||
//Find PropertyTypes that does not belong to a PropertyTypeGroup
|
||||
var propertyTypes = new List<PropertyType>();
|
||||
foreach (var typeDto in dto.PropertyTypes.Where(x => (x.PropertyTypeGroupId.HasValue == false || x.PropertyTypeGroupId.Value == 0) && x.Id.HasValue))
|
||||
{
|
||||
//Internal dictionary for adding "MemberCanEdit" and "VisibleOnProfile" properties to each PropertyType
|
||||
|
||||
memberType.MemberTypePropertyTypes.Add(typeDto.Alias,
|
||||
new MemberTypePropertyProfileAccess(typeDto.ViewOnProfile, typeDto.CanEdit, typeDto.IsSensitive));
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
protected override IIdentityUserLogin PerformGet(int id)
|
||||
{
|
||||
var sql = GetBaseQuery(false);
|
||||
sql.Where(GetBaseWhereClause(), new { Id = id });
|
||||
sql.Where(GetBaseWhereClause(), new { id = id });
|
||||
|
||||
var dto = Database.Fetch<ExternalLoginDto>(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault();
|
||||
if (dto == null)
|
||||
|
||||
@@ -308,13 +308,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
if (dtos == null || dtos.Any() == false)
|
||||
return Enumerable.Empty<IMemberType>();
|
||||
|
||||
return dtos.Select(x =>
|
||||
{
|
||||
bool needsSaving;
|
||||
var memberType = MemberTypeReadOnlyFactory.BuildEntity(x, out needsSaving);
|
||||
if (needsSaving) PersistUpdatedItem(memberType);
|
||||
return memberType;
|
||||
}).ToList();
|
||||
return dtos.Select(MemberTypeReadOnlyFactory.BuildEntity).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
internal class DropDownFlexibleConfiguration : ValueListConfiguration
|
||||
public class DropDownFlexibleConfiguration : ValueListConfiguration
|
||||
{
|
||||
[ConfigurationField("multiple", "Enable multiple choice", "boolean", Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")]
|
||||
public bool Multiple { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Web;
|
||||
using System.Web.Hosting;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration;
|
||||
@@ -70,10 +71,15 @@ namespace Umbraco.Core.Runtime
|
||||
// objects.
|
||||
|
||||
using (var timer = profilingLogger.TraceDuration<CoreRuntime>(
|
||||
$"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()} on {NetworkHelper.MachineName}.",
|
||||
$"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()}.",
|
||||
"Booted.",
|
||||
"Boot failed."))
|
||||
{
|
||||
logger.Info<CoreRuntime>("Booting site '{HostingSiteName}', app '{HostingApplicationID}', path '{HostingPhysicalPath}', server '{MachineName}'.",
|
||||
HostingEnvironment.SiteName,
|
||||
HostingEnvironment.ApplicationID,
|
||||
HostingEnvironment.ApplicationPhysicalPath,
|
||||
NetworkHelper.MachineName);
|
||||
logger.Debug<CoreRuntime>("Runtime: {Runtime}", GetType().FullName);
|
||||
|
||||
// application environment
|
||||
|
||||
@@ -2,39 +2,61 @@
|
||||
|
||||
namespace Umbraco.Core.Scoping
|
||||
{
|
||||
// base class for an object that will be enlisted in scope context, if any. it
|
||||
// must be used in a 'using' block, and if not scoped, released when disposed,
|
||||
// else when scope context runs enlisted actions
|
||||
/// <summary>
|
||||
/// Provides a base class for scope contextual objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>A scope contextual object is enlisted in the current scope context,
|
||||
/// if any, and released when the context exists. It must be used in a 'using'
|
||||
/// block, and will be released when disposed, if not part of a scope.</para>
|
||||
/// </remarks>
|
||||
public abstract class ScopeContextualBase : IDisposable
|
||||
{
|
||||
private bool _using, _scoped;
|
||||
private bool _scoped;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a contextual object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the object.</typeparam>
|
||||
/// <param name="scopeProvider">A scope provider.</param>
|
||||
/// <param name="key">A context key for the object.</param>
|
||||
/// <param name="ctor">A function producing the contextual object.</param>
|
||||
/// <returns>The contextual object.</returns>
|
||||
/// <remarks>
|
||||
/// <para></para>
|
||||
/// </remarks>
|
||||
public static T Get<T>(IScopeProvider scopeProvider, string key, Func<bool, T> ctor)
|
||||
where T : ScopeContextualBase
|
||||
{
|
||||
// no scope context = create a non-scoped object
|
||||
var scopeContext = scopeProvider.Context;
|
||||
if (scopeContext == null)
|
||||
return ctor(false);
|
||||
|
||||
// create & enlist the scoped object
|
||||
var w = scopeContext.Enlist("ScopeContextualBase_" + key,
|
||||
() => ctor(true),
|
||||
(completed, item) => { item.Release(completed); });
|
||||
|
||||
if (w._using) throw new InvalidOperationException("panic: used.");
|
||||
w._using = true;
|
||||
w._scoped = true;
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <para>If not scoped, then this releases the contextual object.</para>
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
_using = false;
|
||||
|
||||
if (_scoped == false)
|
||||
Release(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the contextual object.
|
||||
/// </summary>
|
||||
/// <param name="completed">A value indicating whether the scoped operation completed.</param>
|
||||
public abstract void Release(bool completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1902,9 +1902,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
content.ParentId = parentId;
|
||||
|
||||
// get the level delta (old pos to new pos)
|
||||
var levelDelta = parent == null
|
||||
? 1 - content.Level + (parentId == Constants.System.RecycleBinContent ? 1 : 0)
|
||||
: parent.Level + 1 - content.Level;
|
||||
// note that recycle bin (id:-20) level is 0!
|
||||
var levelDelta = 1 - content.Level + (parent?.Level ?? 0);
|
||||
|
||||
var paths = new Dictionary<int, string>();
|
||||
|
||||
@@ -2875,7 +2874,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
{
|
||||
foreach (var property in blueprint.Properties)
|
||||
{
|
||||
content.SetValue(property.Alias, property.GetValue(culture), culture);
|
||||
if (property.PropertyType.VariesByCulture())
|
||||
{
|
||||
content.SetValue(property.Alias, property.GetValue(culture), culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
content.SetValue(property.Alias, property.GetValue());
|
||||
}
|
||||
}
|
||||
|
||||
content.Name = blueprint.Name;
|
||||
|
||||
@@ -289,7 +289,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
scope.Events.Dispatch(Saved, this, saveEventArgs);
|
||||
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>(media, TreeChangeTypes.RefreshNode).ToEventArgs());
|
||||
}
|
||||
|
||||
|
||||
if (withIdentity == false)
|
||||
return;
|
||||
|
||||
@@ -716,7 +716,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
#endregion
|
||||
|
||||
#region Delete
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Permanently deletes an <see cref="IMedia"/> object
|
||||
/// </summary>
|
||||
@@ -975,9 +975,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
media.ParentId = parentId;
|
||||
|
||||
// get the level delta (old pos to new pos)
|
||||
var levelDelta = parent == null
|
||||
? 1 - media.Level + (parentId == Constants.System.RecycleBinMedia ? 1 : 0)
|
||||
: parent.Level + 1 - media.Level;
|
||||
// note that recycle bin (id:-20) level is 0!
|
||||
var levelDelta = 1 - media.Level + (parent?.Level ?? 0);
|
||||
|
||||
var paths = new Dictionary<int, string>();
|
||||
|
||||
@@ -1024,6 +1023,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// <summary>
|
||||
/// Empties the Recycle Bin by deleting all <see cref="IMedia"/> that resides in the bin
|
||||
/// </summary>
|
||||
/// <param name="userId">Optional Id of the User emptying the Recycle Bin</param>
|
||||
public OperationResult EmptyRecycleBin()
|
||||
{
|
||||
var nodeObjectType = Constants.ObjectTypes.Media;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Web.PublishedCache.NuCache;
|
||||
using Umbraco.Web.PublishedCache.NuCache.Snap;
|
||||
|
||||
namespace Umbraco.Tests.Cache
|
||||
{
|
||||
@@ -388,8 +389,7 @@ namespace Umbraco.Tests.Cache
|
||||
// collect liveGen
|
||||
GC.Collect();
|
||||
|
||||
SnapDictionary<int, string>.GenerationObject genObj;
|
||||
Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
|
||||
Assert.IsTrue(d.Test.GenObjs.TryPeek(out var genObj));
|
||||
genObj = null;
|
||||
|
||||
// in Release mode, it works, but in Debug mode, the weak reference is still alive
|
||||
@@ -399,14 +399,14 @@ namespace Umbraco.Tests.Cache
|
||||
GC.Collect();
|
||||
#endif
|
||||
|
||||
Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
|
||||
Assert.IsFalse(genObj.WeakReference.IsAlive); // snapshot is gone, along with its reference
|
||||
Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj));
|
||||
Assert.IsFalse(genObj.WeakGenRef.IsAlive); // snapshot is gone, along with its reference
|
||||
|
||||
await d.CollectAsync();
|
||||
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length); // null value is gone
|
||||
Assert.AreEqual(0, d.Count); // item is gone
|
||||
Assert.AreEqual(0, d.Test.GenerationObjects.Count);
|
||||
Assert.AreEqual(0, d.Test.GenObjs.Count);
|
||||
Assert.AreEqual(0, d.SnapCount); // snapshot is gone
|
||||
Assert.AreEqual(0, d.GenCount); // and generation has been dequeued
|
||||
}
|
||||
@@ -632,7 +632,7 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
using (d.GetWriter(GetScopeProvider()))
|
||||
using (d.GetScopedWriteLock(GetScopeProvider()))
|
||||
{
|
||||
var s1 = d.CreateSnapshot();
|
||||
|
||||
@@ -685,7 +685,7 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual("uno", s2.Get(1));
|
||||
|
||||
using (d.GetWriter(GetScopeProvider()))
|
||||
using (d.GetScopedWriteLock(GetScopeProvider()))
|
||||
{
|
||||
// gen 3
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
@@ -712,16 +712,102 @@ namespace Umbraco.Tests.Cache
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NestedWriteLocking()
|
||||
public void NestedWriteLocking1()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
var t = d.Test;
|
||||
t.CollectAuto = false;
|
||||
|
||||
Assert.AreEqual(0, d.CreateSnapshot().Gen);
|
||||
|
||||
// no scope context: writers nest, last one to be disposed commits
|
||||
|
||||
var scopeProvider = GetScopeProvider();
|
||||
|
||||
using (var w1 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(2, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
Assert.AreNotSame(w1, w2); // get a new writer each time
|
||||
|
||||
d.Set(1, "one");
|
||||
|
||||
Assert.AreEqual(0, d.CreateSnapshot().Gen);
|
||||
}
|
||||
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
Assert.AreEqual(0, d.CreateSnapshot().Gen);
|
||||
}
|
||||
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(0, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
Assert.AreEqual(1, d.CreateSnapshot().Gen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NestedWriteLocking2()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
var scopeProvider = GetScopeProvider();
|
||||
using (d.GetWriter(scopeProvider))
|
||||
Assert.AreEqual(0, d.CreateSnapshot().Gen);
|
||||
|
||||
// scope context: writers enlist
|
||||
|
||||
var scopeContext = new ScopeContext();
|
||||
var scopeProvider = GetScopeProvider(scopeContext);
|
||||
|
||||
using (var w1 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
using (d.GetWriter(scopeProvider))
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
Assert.AreSame(w1, w2);
|
||||
|
||||
d.Set(1, "one");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NestedWriteLocking3()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
var t = d.Test;
|
||||
t.CollectAuto = false;
|
||||
|
||||
Assert.AreEqual(0, d.CreateSnapshot().Gen);
|
||||
|
||||
var scopeContext = new ScopeContext();
|
||||
var scopeProvider1 = GetScopeProvider();
|
||||
var scopeProvider2 = GetScopeProvider(scopeContext);
|
||||
|
||||
using (var w1 = d.GetScopedWriteLock(scopeProvider1))
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
using (var w2 = d.GetScopedWriteLock(scopeProvider2))
|
||||
{
|
||||
Assert.AreEqual(1, t.LiveGen);
|
||||
Assert.AreEqual(2, t.WLocked);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
|
||||
Assert.AreNotSame(w1, w2);
|
||||
|
||||
d.Set(1, "one");
|
||||
}
|
||||
}
|
||||
@@ -764,7 +850,7 @@ namespace Umbraco.Tests.Cache
|
||||
|
||||
var scopeProvider = GetScopeProvider();
|
||||
|
||||
using (d.GetWriter(scopeProvider))
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// gen 3
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
@@ -809,7 +895,7 @@ namespace Umbraco.Tests.Cache
|
||||
|
||||
var scopeProvider = GetScopeProvider();
|
||||
|
||||
using (d.GetWriter(scopeProvider))
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// creating a snapshot in a write-lock does NOT return the "current" content
|
||||
// it uses the previous snapshot, so new snapshot created only on release
|
||||
@@ -846,9 +932,10 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.AreEqual(2, s2.Gen);
|
||||
Assert.AreEqual("uno", s2.Get(1));
|
||||
|
||||
var scopeProvider = GetScopeProvider(true);
|
||||
var scopeContext = new ScopeContext();
|
||||
var scopeProvider = GetScopeProvider(scopeContext);
|
||||
|
||||
using (d.GetWriter(scopeProvider))
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// creating a snapshot in a write-lock does NOT return the "current" content
|
||||
// it uses the previous snapshot, so new snapshot created only on release
|
||||
@@ -867,7 +954,7 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.AreEqual(2, s4.Gen);
|
||||
Assert.AreEqual("uno", s4.Get(1));
|
||||
|
||||
((ScopeContext) scopeProvider.Context).ScopeExit(true);
|
||||
scopeContext.ScopeExit(true);
|
||||
|
||||
var s5 = d.CreateSnapshot();
|
||||
Assert.AreEqual(3, s5.Gen);
|
||||
@@ -878,7 +965,8 @@ namespace Umbraco.Tests.Cache
|
||||
public void ScopeLocking2()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
var t = d.Test;
|
||||
t.CollectAuto = false;
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
@@ -891,12 +979,13 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.AreEqual(2, s2.Gen);
|
||||
Assert.AreEqual("uno", s2.Get(1));
|
||||
|
||||
var scopeProviderMock = new Mock<IScopeProvider>();
|
||||
var scopeContext = new ScopeContext();
|
||||
scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
|
||||
var scopeProvider = scopeProviderMock.Object;
|
||||
Assert.AreEqual(2, t.LiveGen);
|
||||
Assert.IsFalse(t.NextGen);
|
||||
|
||||
using (d.GetWriter(scopeProvider))
|
||||
var scopeContext = new ScopeContext();
|
||||
var scopeProvider = GetScopeProvider(scopeContext);
|
||||
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
// creating a snapshot in a write-lock does NOT return the "current" content
|
||||
// it uses the previous snapshot, so new snapshot created only on release
|
||||
@@ -905,18 +994,35 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.AreEqual(2, s3.Gen);
|
||||
Assert.AreEqual("uno", s3.Get(1));
|
||||
|
||||
// we made some changes, so a next gen is required
|
||||
Assert.AreEqual(3, t.LiveGen);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
|
||||
// but live snapshot contains changes
|
||||
var ls = d.Test.LiveSnapshot;
|
||||
var ls = t.LiveSnapshot;
|
||||
Assert.AreEqual("ein", ls.Get(1));
|
||||
Assert.AreEqual(3, ls.Gen);
|
||||
}
|
||||
|
||||
// nothing is committed until scope exits
|
||||
Assert.AreEqual(3, t.LiveGen);
|
||||
Assert.IsTrue(t.NextGen);
|
||||
Assert.AreEqual(1, t.WLocked);
|
||||
|
||||
// no changes until exit
|
||||
var s4 = d.CreateSnapshot();
|
||||
Assert.AreEqual(2, s4.Gen);
|
||||
Assert.AreEqual("uno", s4.Get(1));
|
||||
|
||||
scopeContext.ScopeExit(false);
|
||||
|
||||
// now things have changed
|
||||
Assert.AreEqual(2, t.LiveGen);
|
||||
Assert.IsFalse(t.NextGen);
|
||||
Assert.AreEqual(0, t.WLocked);
|
||||
|
||||
// no changes since not completed
|
||||
var s5 = d.CreateSnapshot();
|
||||
Assert.AreEqual(2, s5.Gen);
|
||||
Assert.AreEqual("uno", s5.Get(1));
|
||||
@@ -955,12 +1061,92 @@ namespace Umbraco.Tests.Cache
|
||||
Assert.AreEqual("four", all[3]);
|
||||
}
|
||||
|
||||
private IScopeProvider GetScopeProvider(bool withContext = false)
|
||||
[Test]
|
||||
public void DontPanic()
|
||||
{
|
||||
var scopeProviderMock = new Mock<IScopeProvider>();
|
||||
var scopeContext = withContext ? new ScopeContext() : null;
|
||||
scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
|
||||
var scopeProvider = scopeProviderMock.Object;
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
Assert.IsNull(d.Test.GenObj);
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsNull(d.Test.GenObj);
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(1, d.Test.GenObj.Gen);
|
||||
|
||||
Assert.AreEqual(1, s1.Gen);
|
||||
Assert.AreEqual("one", s1.Get(1));
|
||||
|
||||
d.Set(1, "uno");
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(1, d.Test.GenObj.Gen);
|
||||
|
||||
var scopeContext = new ScopeContext();
|
||||
var scopeProvider = GetScopeProvider(scopeContext);
|
||||
|
||||
// scopeProvider.Context == scopeContext -> writer is scoped
|
||||
// writer is scope contextual and scoped
|
||||
// when disposed, nothing happens
|
||||
// when the context exists, the writer is released
|
||||
using (d.GetScopedWriteLock(scopeProvider))
|
||||
{
|
||||
d.Set(1, "ein");
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(2, d.Test.GenObj.Gen);
|
||||
}
|
||||
|
||||
// writer has not released
|
||||
Assert.AreEqual(1, d.Test.WLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(2, d.Test.GenObj.Gen);
|
||||
|
||||
// nothing changed
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
|
||||
// panic!
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, d.Test.WLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(2, d.Test.GenObj.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
// release writer
|
||||
scopeContext.ScopeExit(true);
|
||||
|
||||
Assert.AreEqual(0, d.Test.WLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(2, d.Test.GenObj.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s3 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(0, d.Test.WLocked);
|
||||
Assert.IsNotNull(d.Test.GenObj);
|
||||
Assert.AreEqual(3, d.Test.GenObj.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
}
|
||||
|
||||
private IScopeProvider GetScopeProvider(ScopeContext scopeContext = null)
|
||||
{
|
||||
var scopeProvider = Mock.Of<IScopeProvider>();
|
||||
Mock.Get(scopeProvider)
|
||||
.Setup(x => x.Context).Returns(scopeContext);
|
||||
return scopeProvider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Compose;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.IO;
|
||||
@@ -299,11 +300,19 @@ namespace Umbraco.Tests.Components
|
||||
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
|
||||
Composed.Clear();
|
||||
Assert.Throws<Exception>(() => composers.Compose());
|
||||
Console.WriteLine("throws:");
|
||||
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
|
||||
var requirements = composers.GetRequirements(false);
|
||||
Console.WriteLine(Composers.GetComposersReport(requirements));
|
||||
|
||||
types = new[] { typeof(Composer2) };
|
||||
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
|
||||
Composed.Clear();
|
||||
Assert.Throws<Exception>(() => composers.Compose());
|
||||
Console.WriteLine("throws:");
|
||||
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
|
||||
requirements = composers.GetRequirements(false);
|
||||
Console.WriteLine(Composers.GetComposersReport(requirements));
|
||||
|
||||
types = new[] { typeof(Composer12) };
|
||||
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
|
||||
@@ -349,6 +358,25 @@ namespace Umbraco.Tests.Components
|
||||
Assert.AreEqual(typeof(Composer27), Composed[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllComposers()
|
||||
{
|
||||
var typeLoader = new TypeLoader(AppCaches.Disabled.RuntimeCache, IOHelper.MapPath("~/App_Data/TEMP"), Mock.Of<IProfilingLogger>());
|
||||
|
||||
var register = MockRegister();
|
||||
var composition = new Composition(register, typeLoader, Mock.Of<IProfilingLogger>(), MockRuntimeState(RuntimeLevel.Run));
|
||||
|
||||
var types = typeLoader.GetTypes<IComposer>().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web"));
|
||||
var composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
|
||||
var requirements = composers.GetRequirements();
|
||||
var report = Composers.GetComposersReport(requirements);
|
||||
Console.WriteLine(report);
|
||||
var composerTypes = composers.SortComposers(requirements);
|
||||
|
||||
foreach (var type in composerTypes)
|
||||
Console.WriteLine(type);
|
||||
}
|
||||
|
||||
#region Compothings
|
||||
|
||||
public class TestComposerBase : IComposer
|
||||
|
||||
@@ -15,13 +15,47 @@ namespace Umbraco.Tests.Models
|
||||
[TestFixture]
|
||||
public class ContentTypeTests : UmbracoTestBase
|
||||
{
|
||||
[Test]
|
||||
[Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")]
|
||||
public void Cannot_Add_Duplicate_Property_Aliases()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[]
|
||||
{
|
||||
new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" }
|
||||
})));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
contentType.PropertyTypeCollection.Add(
|
||||
new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType" }));
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")]
|
||||
public void Cannot_Update_Duplicate_Property_Aliases()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[]
|
||||
{
|
||||
new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" }
|
||||
})));
|
||||
|
||||
contentType.PropertyTypeCollection.Add(new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType2" });
|
||||
|
||||
var toUpdate = contentType.PropertyTypeCollection["myPropertyType2"];
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => toUpdate.Alias = "myPropertyType");
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Deep_Clone_Content_Type_Sort()
|
||||
{
|
||||
var contentType = new ContentTypeSort(new Lazy<int>(() => 3), 4, "test");
|
||||
var clone = (ContentTypeSort) contentType.DeepClone();
|
||||
var clone = (ContentTypeSort)contentType.DeepClone();
|
||||
Assert.AreNotSame(clone, contentType);
|
||||
Assert.AreEqual(clone, contentType);
|
||||
Assert.AreEqual(clone.Id.Value, contentType.Id.Value);
|
||||
@@ -54,7 +88,7 @@ namespace Umbraco.Tests.Models
|
||||
contentType.Id = 10;
|
||||
contentType.CreateDate = DateTime.Now;
|
||||
contentType.CreatorId = 22;
|
||||
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
|
||||
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
|
||||
{
|
||||
Id = 88
|
||||
});
|
||||
@@ -117,12 +151,12 @@ namespace Umbraco.Tests.Models
|
||||
{
|
||||
group.Id = ++i;
|
||||
}
|
||||
contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } };
|
||||
contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } };
|
||||
contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2") };
|
||||
contentType.Id = 10;
|
||||
contentType.CreateDate = DateTime.Now;
|
||||
contentType.CreatorId = 22;
|
||||
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
|
||||
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
|
||||
{
|
||||
Id = 88
|
||||
});
|
||||
@@ -167,12 +201,12 @@ namespace Umbraco.Tests.Models
|
||||
{
|
||||
group.Id = ++i;
|
||||
}
|
||||
contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } };
|
||||
contentType.AllowedContentTypes = new[] {new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2")};
|
||||
contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } };
|
||||
contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2") };
|
||||
contentType.Id = 10;
|
||||
contentType.CreateDate = DateTime.Now;
|
||||
contentType.CreatorId = 22;
|
||||
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
|
||||
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
|
||||
{
|
||||
Id = 88
|
||||
});
|
||||
@@ -264,12 +298,12 @@ namespace Umbraco.Tests.Models
|
||||
{
|
||||
propertyType.Id = ++i;
|
||||
}
|
||||
contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } };
|
||||
contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } };
|
||||
contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2") };
|
||||
contentType.Id = 10;
|
||||
contentType.CreateDate = DateTime.Now;
|
||||
contentType.CreatorId = 22;
|
||||
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
|
||||
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
|
||||
{
|
||||
Id = 88
|
||||
});
|
||||
|
||||
@@ -232,6 +232,35 @@ namespace Umbraco.Tests.Persistence.Repositories
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This demonstates an issue found: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698
|
||||
/// </summary>
|
||||
[Test]
|
||||
[Ignore("Still testing")]
|
||||
public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception()
|
||||
{
|
||||
//TODO: Fix this bug and then change this test
|
||||
|
||||
var provider = TestObjects.GetScopeProvider(Logger);
|
||||
using (var scope = provider.CreateScope())
|
||||
{
|
||||
var repository = CreateRepository(provider);
|
||||
|
||||
IMemberType memberType = MockedContentTypes.CreateSimpleMemberType();
|
||||
repository.Save(memberType);
|
||||
|
||||
foreach (var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs())
|
||||
{
|
||||
var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key);
|
||||
prop.Alias = prop.Alias + "__0000";
|
||||
}
|
||||
|
||||
repository.Save(memberType);
|
||||
|
||||
//Assert.Throws<ArgumentException>(() => );
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Built_In_Member_Type_Properties_Are_Automatically_Added_When_Creating()
|
||||
{
|
||||
|
||||
+7
-4
@@ -62,8 +62,8 @@ function valPropertyMsg(serverValidationManager) {
|
||||
if (!watcher) {
|
||||
watcher = scope.$watch("currentProperty.value",
|
||||
function (newValue, oldValue) {
|
||||
|
||||
if (!newValue || angular.equals(newValue, oldValue)) {
|
||||
|
||||
if (angular.equals(newValue, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,10 +78,12 @@ function valPropertyMsg(serverValidationManager) {
|
||||
// based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
|
||||
// is the only one, then we'll clear.
|
||||
|
||||
if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) {
|
||||
if (errCount === 0 || (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) {
|
||||
scope.errorMsg = "";
|
||||
formCtrl.$setValidity('valPropertyMsg', true);
|
||||
stopWatch();
|
||||
} else if (showValidation && scope.errorMsg === "") {
|
||||
formCtrl.$setValidity('valPropertyMsg', false);
|
||||
scope.errorMsg = getErrorMsg();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
@@ -152,6 +154,7 @@ function valPropertyMsg(serverValidationManager) {
|
||||
showValidation = true;
|
||||
if (hasError && scope.errorMsg === "") {
|
||||
scope.errorMsg = getErrorMsg();
|
||||
startWatch();
|
||||
}
|
||||
else if (!hasError) {
|
||||
scope.errorMsg = "";
|
||||
|
||||
@@ -112,6 +112,7 @@ When building a custom infinite editor view you can use the same components as a
|
||||
type="button"
|
||||
button-style="link"
|
||||
label-key="general_close"
|
||||
shortcut="esc"
|
||||
action="vm.close()">
|
||||
</umb-button>
|
||||
<umb-button
|
||||
|
||||
@@ -55,6 +55,16 @@
|
||||
position: absolute;;
|
||||
}
|
||||
|
||||
.--notInFront .umb-modalcolumn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
background: rgba(0,0,0,.4);
|
||||
}
|
||||
|
||||
/* re-align loader */
|
||||
.umb-modal .umb-loader-wrapper, .umb-modalcolumn .umb-loader-wrapper, .umb-dialog .umb-loader-wrapper{
|
||||
position:relative;
|
||||
|
||||
@@ -46,8 +46,7 @@
|
||||
|
||||
if (data.language !== "undefined") {
|
||||
var lang = vm.languages.filter(function (l) {
|
||||
return matchLanguageById(l, data.language.Id);
|
||||
|
||||
return matchLanguageById(l, data.language);
|
||||
});
|
||||
if (lang.length > 0) {
|
||||
vm.language = lang[0];
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
<umb-box>
|
||||
<umb-box-content>
|
||||
<div class="umb-package-details__section-title"><localize key="packager_packageCompatibility">Compatibility</localize></div>
|
||||
<div class="umb-package-details__section-description"><localize key="packager_packageCompatibilityDescription">This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100%</localize></div>
|
||||
<div class="umb-package-details__section-description"><localize key="packager_packageCompatibilityDescription">This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100%</localize></div>
|
||||
<div class="umb-package-details__compatability" ng-repeat="compatibility in vm.package.compatibility | filter:percentage > 0">
|
||||
<div class="umb-package-details__compatability-label">
|
||||
<span class="umb-package-details__information-item-label">{{compatibility.version}}</span>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
function imageFilePickerController($scope) {
|
||||
function imageFilePickerController($scope, editorService) {
|
||||
|
||||
$scope.add = function() {
|
||||
$scope.mediaPickerOverlay = {
|
||||
var mediaPickerOptions = {
|
||||
view: "mediapicker",
|
||||
multiPicker: false,
|
||||
disableFolderSelect: true,
|
||||
onlyImages: true,
|
||||
show: true,
|
||||
submit: function (model) {
|
||||
$scope.model.value = model.selection[0].image;
|
||||
$scope.mediaPickerOverlay.show = false;
|
||||
$scope.mediaPickerOverlay = null;
|
||||
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
$scope.mediaPickerOverlay.show = false;
|
||||
$scope.mediaPickerOverlay = null;
|
||||
}
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
editorService.mediaPicker(mediaPickerOptions);
|
||||
};
|
||||
|
||||
$scope.remove = function () {
|
||||
|
||||
@@ -19,11 +19,4 @@
|
||||
<i class="icon icon-add large"></i>
|
||||
</a>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="mediaPickerOverlay.show"
|
||||
model="mediaPickerOverlay"
|
||||
view="mediaPickerOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
function ConfigController($scope) {
|
||||
|
||||
var vm = this;
|
||||
|
||||
vm.submit = submit;
|
||||
vm.close = close;
|
||||
|
||||
function submit() {
|
||||
if ($scope.model && $scope.model.submit) {
|
||||
$scope.model.submit($scope.model);
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if ($scope.model.close) {
|
||||
$scope.model.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.ConfigController", ConfigController);
|
||||
@@ -1,16 +1,61 @@
|
||||
<div ng-if="model.config">
|
||||
<umb-property
|
||||
property="configValue"
|
||||
ng-repeat="configValue in model.config">
|
||||
<umb-property-editor model="configValue" is-pre-value="true"></umb-property-editor>
|
||||
</umb-property>
|
||||
</div>
|
||||
<div ng-controller="Umbraco.PropertyEditors.GridPrevalueEditor.ConfigController as vm">
|
||||
|
||||
<umb-editor-view>
|
||||
|
||||
<umb-editor-header
|
||||
name="model.title"
|
||||
name-locked="true"
|
||||
hide-alias="true"
|
||||
hide-icon="true"
|
||||
hide-description="true">
|
||||
</umb-editor-header>
|
||||
|
||||
<umb-editor-container>
|
||||
<ng-form name="gridItemConfigEditor" val-form-manager>
|
||||
|
||||
<umb-box ng-if="model.config">
|
||||
<umb-box-header title-key="grid_settings" />
|
||||
<umb-box-content>
|
||||
<div>
|
||||
<umb-property property="configValue"
|
||||
ng-repeat="configValue in model.config">
|
||||
<umb-property-editor model="configValue" is-pre-value="true"></umb-property-editor>
|
||||
</umb-property>
|
||||
</div>
|
||||
</umb-box-content>
|
||||
</umb-box>
|
||||
|
||||
<umb-box ng-if="model.styles">
|
||||
<umb-box-header title-key="grid_styles" />
|
||||
<umb-box-content>
|
||||
<div>
|
||||
<umb-property property="styleValue"
|
||||
ng-repeat="styleValue in model.styles">
|
||||
<umb-property-editor model="styleValue" is-pre-value="true"></umb-property-editor>
|
||||
</umb-property>
|
||||
</div>
|
||||
</umb-box-content>
|
||||
</umb-box>
|
||||
|
||||
</ng-form>
|
||||
</umb-editor-container>
|
||||
|
||||
<umb-editor-footer>
|
||||
<umb-editor-footer-content-right>
|
||||
<umb-button type="button"
|
||||
button-style="link"
|
||||
label-key="general_close"
|
||||
shortcut="esc"
|
||||
action="vm.close()">
|
||||
</umb-button>
|
||||
<umb-button type="button"
|
||||
button-style="success"
|
||||
label-key="general_submit"
|
||||
action="vm.submit()">
|
||||
</umb-button>
|
||||
</umb-editor-footer-content-right>
|
||||
</umb-editor-footer>
|
||||
|
||||
</umb-editor-view>
|
||||
|
||||
<div ng-if="model.styles">
|
||||
<h4>Style</h4>
|
||||
<umb-property
|
||||
property="styleValue"
|
||||
ng-repeat="styleValue in model.styles">
|
||||
<umb-property-editor model="styleValue" is-pre-value="true"></umb-property-editor>
|
||||
</umb-property>
|
||||
</div>
|
||||
|
||||
+14
-8
@@ -1,16 +1,22 @@
|
||||
function EditConfigController($scope) {
|
||||
|
||||
$scope.close = function() {
|
||||
if($scope.model.close) {
|
||||
$scope.model.close();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.submit = function() {
|
||||
if($scope.model && $scope.model.submit) {
|
||||
var vm = this;
|
||||
|
||||
vm.submit = submit;
|
||||
vm.close = close;
|
||||
|
||||
function submit() {
|
||||
if ($scope.model && $scope.model.submit) {
|
||||
$scope.model.submit($scope.model);
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if ($scope.model.close) {
|
||||
$scope.model.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.EditConfigController", EditConfigController);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<div class="usky-grid usky-grid-configuration" ng-controller="Umbraco.PropertyEditors.GridPrevalueEditor.EditConfigController">
|
||||
<div class="usky-grid usky-grid-configuration" ng-controller="Umbraco.PropertyEditors.GridPrevalueEditor.EditConfigController as vm">
|
||||
|
||||
<umb-editor-view>
|
||||
|
||||
|
||||
<umb-editor-view>
|
||||
|
||||
<form novalidate name="EditConfigForm" val-form-manager>
|
||||
<form novalidate name="EditConfigForm" val-form-manager>
|
||||
|
||||
<umb-editor-header
|
||||
name="model.title"
|
||||
@@ -18,44 +16,39 @@
|
||||
<umb-box>
|
||||
<umb-box-content>
|
||||
|
||||
<ng-form name="gridConfigEditor">
|
||||
|
||||
<h4>{{model.name}}</h4>
|
||||
<p>Settings will only save if the entered json configuration is valid</p>
|
||||
<textarea name="configSource"
|
||||
validate-on="'blur'"
|
||||
rows="20" class="umb-property-editor umb-textarea"
|
||||
umb-raw-model="model.config"></textarea>
|
||||
|
||||
<form name="gridConfigEditor">
|
||||
<div class="alert alert-error" ng-show="gridConfigEditor.$invalid === true">
|
||||
This configuration is not valid json, and will not be saved.
|
||||
</div>
|
||||
|
||||
<h4>{{model.name}}</h4>
|
||||
<p>Settings will only save if the entered json configuration is valid</p>
|
||||
<textarea name="configSource"
|
||||
validate-on="'blur'"
|
||||
rows="20" class="umb-property-editor umb-textarea"
|
||||
umb-raw-model="model.config"></textarea>
|
||||
</ng-form>
|
||||
|
||||
<div class="alert alert-error" ng-show="gridConfigEditor.$invalid === true">
|
||||
This configuration is not valid json, and will not be saved.
|
||||
</div>
|
||||
</umb-box-content>
|
||||
</umb-box>
|
||||
</umb-editor-container>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
</umb-box-content>
|
||||
</umb-box>
|
||||
</umb-editor-container>
|
||||
|
||||
<umb-editor-footer>
|
||||
<umb-editor-footer-content-right>
|
||||
<umb-editor-footer>
|
||||
<umb-editor-footer-content-right>
|
||||
<umb-button
|
||||
type="button"
|
||||
button-style="link"
|
||||
label-key="general_close"
|
||||
shortcut="esc"
|
||||
action="close()">
|
||||
action="vm.close()">
|
||||
</umb-button>
|
||||
<umb-button
|
||||
type="button"
|
||||
button-style="success"
|
||||
label-key="general_submit"
|
||||
action="submit()">
|
||||
action="vm.submit()">
|
||||
</umb-button>
|
||||
</umb-editor-footer-content-right>
|
||||
</umb-editor-footer>
|
||||
|
||||
@@ -8,17 +8,20 @@ angular.module("umbraco")
|
||||
umbRequestHelper,
|
||||
angularHelper,
|
||||
$element,
|
||||
eventsService
|
||||
eventsService,
|
||||
editorService
|
||||
) {
|
||||
|
||||
// Grid status variables
|
||||
var placeHolder = "";
|
||||
var currentForm = angularHelper.getCurrentForm($scope);
|
||||
|
||||
|
||||
$scope.currentRow = null;
|
||||
$scope.currentCell = null;
|
||||
$scope.currentToolsControl = null;
|
||||
$scope.currentControl = null;
|
||||
|
||||
$scope.openRTEToolbarId = null;
|
||||
$scope.hasSettings = false;
|
||||
$scope.showRowConfigurations = true;
|
||||
@@ -238,8 +241,10 @@ angular.module("umbraco")
|
||||
$scope.$apply(function () {
|
||||
|
||||
var cell = event.target.getScope_HackForSortable().area;
|
||||
|
||||
cell.hasActiveChild = hasActiveChild(cell, cell.controls);
|
||||
cell.active = false;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -273,7 +278,7 @@ angular.module("umbraco")
|
||||
localizationService.localize("grid_insertControl").then(function(value){
|
||||
title = value;
|
||||
$scope.editorOverlay = {
|
||||
view: "itempicker",
|
||||
view: "itempicker",
|
||||
filter: area.$allowedEditors.length > 15,
|
||||
title: title,
|
||||
availableItems: area.$allowedEditors,
|
||||
@@ -314,6 +319,7 @@ angular.module("umbraco")
|
||||
|
||||
$scope.clickOutsideRow = function(index, rows) {
|
||||
rows[index].active = false;
|
||||
|
||||
};
|
||||
|
||||
function getAllowedLayouts(section) {
|
||||
@@ -431,44 +437,46 @@ angular.module("umbraco")
|
||||
});
|
||||
}
|
||||
|
||||
$scope.gridItemSettingsDialog = {};
|
||||
$scope.gridItemSettingsDialog.view = "views/propertyeditors/grid/dialogs/config.html";
|
||||
$scope.gridItemSettingsDialog.title = "Settings";
|
||||
$scope.gridItemSettingsDialog.styles = styles;
|
||||
$scope.gridItemSettingsDialog.config = config;
|
||||
var dialogOptions = {
|
||||
view: "views/propertyeditors/grid/dialogs/config.html",
|
||||
size: "small",
|
||||
styles: styles,
|
||||
config: config,
|
||||
submit: function (model) {
|
||||
var styleObject = {};
|
||||
var configObject = {};
|
||||
|
||||
$scope.gridItemSettingsDialog.show = true;
|
||||
_.each(model.styles, function (style) {
|
||||
if (style.value) {
|
||||
styleObject[style.key] = addModifier(style.value, style.modifier);
|
||||
}
|
||||
});
|
||||
_.each(model.config, function (cfg) {
|
||||
cfg.alias = cfg.key;
|
||||
cfg.label = cfg.value;
|
||||
|
||||
$scope.gridItemSettingsDialog.submit = function(model) {
|
||||
if (cfg.value) {
|
||||
configObject[cfg.key] = addModifier(cfg.value, cfg.modifier);
|
||||
}
|
||||
});
|
||||
|
||||
var styleObject = {};
|
||||
var configObject = {};
|
||||
gridItem.styles = styleObject;
|
||||
gridItem.config = configObject;
|
||||
gridItem.hasConfig = gridItemHasConfig(styleObject, configObject);
|
||||
|
||||
_.each(model.styles, function(style){
|
||||
if(style.value){
|
||||
styleObject[style.key] = addModifier(style.value, style.modifier);
|
||||
currentForm.$setDirty();
|
||||
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
editorService.close();
|
||||
}
|
||||
});
|
||||
_.each(model.config, function (cfg) {
|
||||
if (cfg.value) {
|
||||
configObject[cfg.key] = addModifier(cfg.value, cfg.modifier);
|
||||
}
|
||||
});
|
||||
|
||||
gridItem.styles = styleObject;
|
||||
gridItem.config = configObject;
|
||||
gridItem.hasConfig = gridItemHasConfig(styleObject, configObject);
|
||||
|
||||
currentForm.$setDirty();
|
||||
|
||||
$scope.gridItemSettingsDialog.show = false;
|
||||
$scope.gridItemSettingsDialog = null;
|
||||
};
|
||||
|
||||
$scope.gridItemSettingsDialog.close = function(oldModel) {
|
||||
$scope.gridItemSettingsDialog.show = false;
|
||||
$scope.gridItemSettingsDialog = null;
|
||||
};
|
||||
localizationService.localize("general_settings").then(value => {
|
||||
dialogOptions.title = value;
|
||||
editorService.open(dialogOptions);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
@@ -538,6 +546,7 @@ angular.module("umbraco")
|
||||
// Control management functions
|
||||
// *********************************************
|
||||
$scope.clickControl = function (index, controls, cell) {
|
||||
|
||||
controls[index].active = true;
|
||||
cell.hasActiveChild = true;
|
||||
};
|
||||
|
||||
@@ -305,11 +305,4 @@
|
||||
position="target">
|
||||
</umb-overlay>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="gridItemSettingsDialog.show"
|
||||
model="gridItemSettingsDialog"
|
||||
view="gridItemSettingsDialog.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs
|
||||
allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish,
|
||||
allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish,
|
||||
allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy,
|
||||
allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove,
|
||||
allowBulkMove: $scope.entityType !== 'member' && $scope.model.config.bulkActionPermissions.allowBulkMove,
|
||||
allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete,
|
||||
cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture
|
||||
};
|
||||
@@ -583,6 +583,8 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs
|
||||
.then(function () {
|
||||
//executes if all is successful, let's sync the tree
|
||||
if (newPath) {
|
||||
// reload the current view so the moved items are no longer shown
|
||||
$scope.reloadView($scope.contentId);
|
||||
|
||||
//we need to do a double sync here: first refresh the node where the content was moved,
|
||||
// then refresh the node where the content was moved from
|
||||
|
||||
@@ -340,9 +340,9 @@
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>8010</DevelopmentServerPort>
|
||||
<DevelopmentServerPort>8020</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8010</IISUrl>
|
||||
<IISUrl>http://localhost:8020</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>False</UseCustomServer>
|
||||
<CustomServerUrl>
|
||||
|
||||
@@ -1164,7 +1164,7 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="packageDownloads">Downloads</key>
|
||||
<key alias="packageLikes">Likes</key>
|
||||
<key alias="packageCompatibility">Compatibility</key>
|
||||
<key alias="packageCompatibilityDescription">This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100%</key>
|
||||
<key alias="packageCompatibilityDescription">This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100%</key>
|
||||
<key alias="packageExternalSources">External sources</key>
|
||||
<key alias="packageAuthor">Author</key>
|
||||
<key alias="packageDocumentation">Documentation</key>
|
||||
|
||||
@@ -1163,7 +1163,7 @@ To manage your website, simply open the Umbraco back office and start adding con
|
||||
<key alias="packageDownloads">Downloads</key>
|
||||
<key alias="packageLikes">Likes</key>
|
||||
<key alias="packageCompatibility">Compatibility</key>
|
||||
<key alias="packageCompatibilityDescription">This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100%</key>
|
||||
<key alias="packageCompatibilityDescription">This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100%</key>
|
||||
<key alias="packageExternalSources">External sources</key>
|
||||
<key alias="packageAuthor">Author</key>
|
||||
<key alias="packageDocumentation">Documentation</key>
|
||||
|
||||
@@ -13,6 +13,7 @@ using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.Dtos;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Web.Composing;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Web
|
||||
{
|
||||
@@ -26,10 +27,17 @@ namespace Umbraco.Web
|
||||
{
|
||||
private readonly IUmbracoDatabaseFactory _databaseFactory;
|
||||
|
||||
[Obsolete("This overload should not be used, enableDistCalls has no effect")]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public BatchedDatabaseServerMessenger(
|
||||
IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings,
|
||||
bool enableDistCalls, DatabaseServerMessengerOptions options)
|
||||
: base(runtime, scopeProvider, sqlContext, proflog, globalSettings, enableDistCalls, options)
|
||||
IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, bool enableDistCalls, DatabaseServerMessengerOptions options)
|
||||
: this(runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings, options)
|
||||
{
|
||||
}
|
||||
|
||||
public BatchedDatabaseServerMessenger(
|
||||
IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, DatabaseServerMessengerOptions options)
|
||||
: base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options)
|
||||
{
|
||||
_databaseFactory = databaseFactory;
|
||||
}
|
||||
|
||||
@@ -38,54 +38,44 @@ namespace Umbraco.Web.Compose
|
||||
|
||||
public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer<DatabaseServerRegistrarAndMessengerComponent>, ICoreComposer
|
||||
{
|
||||
public static DatabaseServerMessengerOptions GetDefaultOptions(IFactory factory)
|
||||
{
|
||||
var logger = factory.GetInstance<ILogger>();
|
||||
var indexRebuilder = factory.GetInstance<IndexRebuilder>();
|
||||
|
||||
return new DatabaseServerMessengerOptions
|
||||
{
|
||||
//These callbacks will be executed if the server has not been synced
|
||||
// (i.e. it is a new server or the lastsynced.txt file has been removed)
|
||||
InitializingCallbacks = new Action[]
|
||||
{
|
||||
//rebuild the xml cache file if the server is not synced
|
||||
() =>
|
||||
{
|
||||
// rebuild the published snapshot caches entirely, if the server is not synced
|
||||
// this is equivalent to DistributedCache RefreshAll... but local only
|
||||
// (we really should have a way to reuse RefreshAll... locally)
|
||||
// note: refresh all content & media caches does refresh content types too
|
||||
var svc = Current.PublishedSnapshotService;
|
||||
svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) });
|
||||
svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _);
|
||||
svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _);
|
||||
},
|
||||
|
||||
//rebuild indexes if the server is not synced
|
||||
// NOTE: This will rebuild ALL indexes including the members, if developers want to target specific
|
||||
// indexes then they can adjust this logic themselves.
|
||||
() => { ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override void Compose(Composition composition)
|
||||
{
|
||||
base.Compose(composition);
|
||||
|
||||
composition.SetServerMessenger(factory =>
|
||||
{
|
||||
var runtime = factory.GetInstance<IRuntimeState>();
|
||||
var databaseFactory = factory.GetInstance<IUmbracoDatabaseFactory>();
|
||||
var globalSettings = factory.GetInstance<IGlobalSettings>();
|
||||
var proflog = factory.GetInstance<IProfilingLogger>();
|
||||
var scopeProvider = factory.GetInstance<IScopeProvider>();
|
||||
var sqlContext = factory.GetInstance<ISqlContext>();
|
||||
var logger = factory.GetInstance<ILogger>();
|
||||
var indexRebuilder = factory.GetInstance<IndexRebuilder>();
|
||||
|
||||
return new BatchedDatabaseServerMessenger(
|
||||
runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings,
|
||||
true,
|
||||
//Default options for web including the required callbacks to build caches
|
||||
new DatabaseServerMessengerOptions
|
||||
{
|
||||
//These callbacks will be executed if the server has not been synced
|
||||
// (i.e. it is a new server or the lastsynced.txt file has been removed)
|
||||
InitializingCallbacks = new Action[]
|
||||
{
|
||||
//rebuild the xml cache file if the server is not synced
|
||||
() =>
|
||||
{
|
||||
// rebuild the published snapshot caches entirely, if the server is not synced
|
||||
// this is equivalent to DistributedCache RefreshAll... but local only
|
||||
// (we really should have a way to reuse RefreshAll... locally)
|
||||
// note: refresh all content & media caches does refresh content types too
|
||||
var svc = Current.PublishedSnapshotService;
|
||||
svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) });
|
||||
svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _);
|
||||
svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _);
|
||||
},
|
||||
|
||||
//rebuild indexes if the server is not synced
|
||||
// NOTE: This will rebuild ALL indexes including the members, if developers want to target specific
|
||||
// indexes then they can adjust this logic themselves.
|
||||
() =>
|
||||
{
|
||||
ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
composition.SetDatabaseServerMessengerOptions(GetDefaultOptions);
|
||||
composition.SetServerMessenger<BatchedDatabaseServerMessenger>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +118,7 @@ namespace Umbraco.Web.Compose
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
{
|
||||
//We will start the whole process when a successful request is made
|
||||
if (_registrar != null || _messenger != null)
|
||||
UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
@@ -14,11 +15,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
[DefaultPropertyValueConverter]
|
||||
public class MediaPickerValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
// hard-coding "image" here but that's how it works at UI level too
|
||||
private const string ImageTypeAlias = "image";
|
||||
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
|
||||
public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor)
|
||||
public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IPublishedModelFactory publishedModelFactory)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor ??
|
||||
throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
}
|
||||
|
||||
public override bool IsConverter(PublishedPropertyType propertyType)
|
||||
@@ -31,15 +39,19 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
var isMultiple = IsMultipleDataType(propertyType.DataType);
|
||||
var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType);
|
||||
|
||||
// hard-coding "image" here but that's how it works at UI level too
|
||||
|
||||
return isMultiple
|
||||
? (isOnlyImages ? typeof(IEnumerable<>).MakeGenericType(ModelType.For("image")) : typeof(IEnumerable<IPublishedContent>))
|
||||
: (isOnlyImages ? ModelType.For("image") : typeof(IPublishedContent));
|
||||
? isOnlyImages
|
||||
? typeof(IEnumerable<>).MakeGenericType(ModelType.For(ImageTypeAlias))
|
||||
: typeof(IEnumerable<IPublishedContent>)
|
||||
: isOnlyImages
|
||||
? ModelType.For(ImageTypeAlias)
|
||||
: typeof(IPublishedContent);
|
||||
}
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Snapshot;
|
||||
{
|
||||
return PropertyCacheLevel.Snapshot;
|
||||
}
|
||||
|
||||
private bool IsMultipleDataType(PublishedDataType dataType)
|
||||
{
|
||||
@@ -53,26 +65,31 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
return config.OnlyImages;
|
||||
}
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType,
|
||||
object source, bool preview)
|
||||
{
|
||||
if (source == null) return null;
|
||||
|
||||
var nodeIds = source.ToString()
|
||||
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(Udi.Parse)
|
||||
.ToArray();
|
||||
return nodeIds;
|
||||
}
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview)
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType,
|
||||
PropertyCacheLevel cacheLevel, object source, bool preview)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var isMultiple = IsMultipleDataType(propertyType.DataType);
|
||||
var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType);
|
||||
|
||||
var udis = (Udi[]) source;
|
||||
var mediaItems = isOnlyImages
|
||||
? _publishedModelFactory.CreateModelList(ImageTypeAlias)
|
||||
: new List<IPublishedContent>();
|
||||
|
||||
if (source == null) return isMultiple ? mediaItems : null;
|
||||
|
||||
var udis = (Udi[])source;
|
||||
var mediaItems = new List<IPublishedContent>();
|
||||
if (udis.Any())
|
||||
{
|
||||
foreach (var udi in udis)
|
||||
@@ -84,12 +101,15 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
mediaItems.Add(item);
|
||||
}
|
||||
|
||||
if (IsMultipleDataType(propertyType.DataType))
|
||||
return mediaItems;
|
||||
return mediaItems.FirstOrDefault();
|
||||
return isMultiple ? mediaItems : FirstOrDefault(mediaItems);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private object FirstOrDefault(IList mediaItems)
|
||||
{
|
||||
return mediaItems.Count == 0 ? null : mediaItems[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using CSharpTest.Net.Collections;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Web.PublishedCache.NuCache.Snap;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
@@ -29,8 +30,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private BPlusTree<int, ContentNodeKit> _localDb;
|
||||
private readonly ConcurrentQueue<GenRefRef> _genRefRefs;
|
||||
private GenRefRef _genRefRef;
|
||||
private readonly ConcurrentQueue<GenObj> _genObjs;
|
||||
private GenObj _genObj;
|
||||
private readonly object _wlocko = new object();
|
||||
private readonly object _rlocko = new object();
|
||||
private long _liveGen, _floorGen;
|
||||
@@ -64,8 +65,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
_contentTypesByAlias = new ConcurrentDictionary<string, LinkedNode<PublishedContentType>>(StringComparer.InvariantCultureIgnoreCase);
|
||||
_xmap = new ConcurrentDictionary<Guid, int>();
|
||||
|
||||
_genRefRefs = new ConcurrentQueue<GenRefRef>();
|
||||
_genRefRef = null; // no initial gen exists
|
||||
_genObjs = new ConcurrentQueue<GenObj>();
|
||||
_genObj = null; // no initial gen exists
|
||||
_liveGen = _floorGen = 0;
|
||||
_nextGen = false; // first time, must create a snapshot
|
||||
_collectAuto = true; // collect automatically by default
|
||||
@@ -91,12 +92,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
|
||||
// a scope contextual that represents a locked writer to the dictionary
|
||||
private class ContentStoreWriter : ScopeContextualBase
|
||||
private class ScopedWriteLock : ScopeContextualBase
|
||||
{
|
||||
private readonly WriteLockInfo _lockinfo = new WriteLockInfo();
|
||||
private ContentStore _store;
|
||||
private readonly ContentStore _store;
|
||||
private int _released;
|
||||
|
||||
public ContentStoreWriter(ContentStore store, bool scoped)
|
||||
public ScopedWriteLock(ContentStore store, bool scoped)
|
||||
{
|
||||
_store = store;
|
||||
store.Lock(_lockinfo, scoped);
|
||||
@@ -104,17 +106,17 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
public override void Release(bool completed)
|
||||
{
|
||||
if (_store== null) return;
|
||||
if (Interlocked.CompareExchange(ref _released, 1, 0) != 0)
|
||||
return;
|
||||
_store.Release(_lockinfo, completed);
|
||||
_store = null;
|
||||
}
|
||||
}
|
||||
|
||||
// gets a scope contextual representing a locked writer to the dictionary
|
||||
// TODO: GetScopedWriter? should the dict have a ref onto the scope provider?
|
||||
public IDisposable GetWriter(IScopeProvider scopeProvider)
|
||||
public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider)
|
||||
{
|
||||
return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ContentStoreWriter(this, scoped));
|
||||
return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped));
|
||||
}
|
||||
|
||||
private void Lock(WriteLockInfo lockInfo, bool forceGen = false)
|
||||
@@ -131,12 +133,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
_wlocked++;
|
||||
lockInfo.Count = true;
|
||||
if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects
|
||||
if (_nextGen == false || (forceGen && _wlocked == 1))
|
||||
{
|
||||
// because we are changing things, a new generation
|
||||
// is created, which will trigger a new snapshot
|
||||
_nextGen = true;
|
||||
if (_nextGen)
|
||||
_genObjs.Enqueue(_genObj = new GenObj(_liveGen));
|
||||
_liveGen += 1;
|
||||
_nextGen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,7 +216,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
else
|
||||
dictionary.TryUpdate(key, link.Next, link);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -310,6 +313,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
var removedContentTypeNodes = new List<int>();
|
||||
var refreshedContentTypeNodes = new List<int>();
|
||||
|
||||
// find all the nodes that are either refreshed or removed,
|
||||
// because of their content type being either refreshed or removed
|
||||
foreach (var link in _contentNodes.Values)
|
||||
{
|
||||
var node = link.Value;
|
||||
@@ -319,39 +324,49 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id);
|
||||
}
|
||||
|
||||
// all content should have been deleted - but
|
||||
// perform deletion of content with removed content type
|
||||
// removing content types should have removed their content already
|
||||
// but just to be 100% sure, clear again here
|
||||
foreach (var node in removedContentTypeNodes)
|
||||
ClearBranchLocked(node);
|
||||
|
||||
// perform deletion of removed content types
|
||||
foreach (var id in removedIdsA)
|
||||
{
|
||||
if (_contentTypesById.TryGetValue(id, out LinkedNode<PublishedContentType> link) == false || link.Value == null)
|
||||
if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null)
|
||||
continue;
|
||||
SetValueLocked(_contentTypesById, id, null);
|
||||
SetValueLocked(_contentTypesByAlias, link.Value.Alias, null);
|
||||
}
|
||||
|
||||
// perform update of refreshed content types
|
||||
foreach (var type in refreshedTypesA)
|
||||
{
|
||||
SetValueLocked(_contentTypesById, type.Id, type);
|
||||
SetValueLocked(_contentTypesByAlias, type.Alias, type);
|
||||
}
|
||||
|
||||
// skip missing type
|
||||
// skip missing parents & unbuildable kits - what else could we do?
|
||||
// perform update of content with refreshed content type - from the kits
|
||||
// skip missing type, skip missing parents & unbuildable kits - what else could we do?
|
||||
// kits are ordered by level, so ParentExits is ok here
|
||||
var visited = new List<int>();
|
||||
foreach (var kit in kits.Where(x =>
|
||||
refreshedIdsA.Contains(x.ContentTypeId) &&
|
||||
ParentExistsLocked(x) &&
|
||||
BuildKit(x)))
|
||||
{
|
||||
// replacing the node: must preserve the parents
|
||||
var node = GetHead(_contentNodes, kit.Node.Id)?.Value;
|
||||
if (node != null)
|
||||
kit.Node.ChildContentIds = node.ChildContentIds;
|
||||
|
||||
SetValueLocked(_contentNodes, kit.Node.Id, kit.Node);
|
||||
|
||||
visited.Add(kit.Node.Id);
|
||||
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
|
||||
}
|
||||
|
||||
// all content should have been refreshed - but...
|
||||
|
||||
var orphans = refreshedContentTypeNodes.Except(visited);
|
||||
foreach (var id in orphans)
|
||||
ClearBranchLocked(id);
|
||||
@@ -500,6 +515,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT kits must be sorted out by LEVEL
|
||||
public void SetAll(IEnumerable<ContentNodeKit> kits)
|
||||
{
|
||||
var lockInfo = new WriteLockInfo();
|
||||
@@ -530,6 +546,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT kits must be sorted out by LEVEL
|
||||
public void SetBranch(int rootContentId, IEnumerable<ContentNodeKit> kits)
|
||||
{
|
||||
var lockInfo = new WriteLockInfo();
|
||||
@@ -836,8 +853,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
// if no next generation is required, and we already have one,
|
||||
// use it and create a new snapshot
|
||||
if (_nextGen == false && _genRefRef != null)
|
||||
return new Snapshot(this, _genRefRef.GetGenRef()
|
||||
if (_nextGen == false && _genObj != null)
|
||||
return new Snapshot(this, _genObj.GetGenRef()
|
||||
#if DEBUG
|
||||
, _logger
|
||||
#endif
|
||||
@@ -852,15 +869,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
var snapGen = _nextGen ? _liveGen - 1 : _liveGen;
|
||||
|
||||
// create a new gen ref unless we already have it
|
||||
if (_genRefRef == null)
|
||||
_genRefRefs.Enqueue(_genRefRef = new GenRefRef(snapGen));
|
||||
else if (_genRefRef.Gen != snapGen)
|
||||
if (_genObj == null)
|
||||
_genObjs.Enqueue(_genObj = new GenObj(snapGen));
|
||||
else if (_genObj.Gen != snapGen)
|
||||
throw new Exception("panic");
|
||||
}
|
||||
else
|
||||
{
|
||||
// not write-locked, can use latest gen, create a new gen ref
|
||||
_genRefRefs.Enqueue(_genRefRef = new GenRefRef(_liveGen));
|
||||
_genObjs.Enqueue(_genObj = new GenObj(_liveGen));
|
||||
_nextGen = false; // this is the ONLY thing that triggers a _liveGen++
|
||||
}
|
||||
|
||||
@@ -873,7 +890,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// - the genRefRef weak ref is dead because all snapshots have been collected
|
||||
// in both cases, we will dequeue and collect
|
||||
|
||||
var snapshot = new Snapshot(this, _genRefRef.GetGenRef()
|
||||
var snapshot = new Snapshot(this, _genObj.GetGenRef()
|
||||
#if DEBUG
|
||||
, _logger
|
||||
#endif
|
||||
@@ -930,10 +947,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
#if DEBUG
|
||||
_logger.Debug<ContentStore>("Collect.");
|
||||
#endif
|
||||
while (_genRefRefs.TryPeek(out GenRefRef genRefRef) && (genRefRef.Count == 0 || genRefRef.WGenRef.IsAlive == false))
|
||||
while (_genObjs.TryPeek(out var genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false))
|
||||
{
|
||||
_genRefRefs.TryDequeue(out genRefRef); // cannot fail since TryPeek has succeeded
|
||||
_floorGen = genRefRef.Gen;
|
||||
_genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded
|
||||
_floorGen = genObj.Gen;
|
||||
#if DEBUG
|
||||
//_logger.Debug<ContentStore>("_floorGen=" + _floorGen + ", _liveGen=" + _liveGen);
|
||||
#endif
|
||||
@@ -1009,9 +1026,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
await task;
|
||||
}
|
||||
|
||||
public long GenCount => _genRefRefs.Count;
|
||||
public long GenCount => _genObjs.Count;
|
||||
|
||||
public long SnapCount => _genRefRefs.Sum(x => x.Count);
|
||||
public long SnapCount => _genObjs.Sum(x => x.Count);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1061,24 +1078,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
#region Classes
|
||||
|
||||
private class LinkedNode<TValue>
|
||||
where TValue: class
|
||||
{
|
||||
public LinkedNode(TValue value, long gen, LinkedNode<TValue> next = null)
|
||||
{
|
||||
Value = value;
|
||||
Gen = gen;
|
||||
Next = next;
|
||||
}
|
||||
|
||||
internal readonly long Gen;
|
||||
|
||||
// reading & writing references is thread-safe on all .NET platforms
|
||||
// mark as volatile to ensure we always read the correct value
|
||||
internal volatile TValue Value;
|
||||
internal volatile LinkedNode<TValue> Next;
|
||||
}
|
||||
|
||||
public class Snapshot : IDisposable
|
||||
{
|
||||
private readonly ContentStore _store;
|
||||
@@ -1100,7 +1099,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
_store = store;
|
||||
_genRef = genRef;
|
||||
_gen = genRef.Gen;
|
||||
Interlocked.Increment(ref genRef.GenRefRef.Count);
|
||||
Interlocked.Increment(ref genRef.GenObj.Count);
|
||||
//_thisCount = _count++;
|
||||
|
||||
#if DEBUG
|
||||
@@ -1201,49 +1200,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
if (_gen < 0) return;
|
||||
#if DEBUG
|
||||
_logger.Debug<Snapshot>("Dispose snapshot ({Snapshot})", _genRef?.GenRefRef.Count.ToString() ?? "live");
|
||||
_logger.Debug<Snapshot>("Dispose snapshot ({Snapshot})", _genRef?.GenObj.Count.ToString() ?? "live");
|
||||
#endif
|
||||
_gen = -1;
|
||||
if (_genRef != null)
|
||||
Interlocked.Decrement(ref _genRef.GenRefRef.Count);
|
||||
Interlocked.Decrement(ref _genRef.GenObj.Count);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal class GenRefRef
|
||||
{
|
||||
public GenRefRef(long gen)
|
||||
{
|
||||
Gen = gen;
|
||||
WGenRef = new WeakReference(null);
|
||||
}
|
||||
|
||||
public GenRef GetGenRef()
|
||||
{
|
||||
// not thread-safe but always invoked from within a lock
|
||||
var genRef = (GenRef) WGenRef.Target;
|
||||
if (genRef == null)
|
||||
WGenRef.Target = genRef = new GenRef(this, Gen);
|
||||
return genRef;
|
||||
}
|
||||
|
||||
public readonly long Gen;
|
||||
public readonly WeakReference WGenRef;
|
||||
public int Count;
|
||||
}
|
||||
|
||||
internal class GenRef
|
||||
{
|
||||
public GenRef(GenRefRef genRefRef, long gen)
|
||||
{
|
||||
GenRefRef = genRefRef;
|
||||
Gen = gen;
|
||||
}
|
||||
|
||||
public readonly GenRefRef GenRefRef;
|
||||
public readonly long Gen;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CSharpTest.Net.Collections;
|
||||
using System.Configuration;
|
||||
using CSharpTest.Net.Collections;
|
||||
using CSharpTest.Net.Serialization;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
@@ -14,6 +15,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
CreateFile = exists ? CreatePolicy.IfNeeded : CreatePolicy.Always,
|
||||
FileName = filepath,
|
||||
|
||||
// read or write but do *not* keep in memory
|
||||
CachePolicy = CachePolicy.None,
|
||||
|
||||
// default is 4096, min 2^9 = 512, max 2^16 = 64K
|
||||
FileBlockSize = GetBlockSize(),
|
||||
|
||||
// other options?
|
||||
};
|
||||
|
||||
@@ -25,6 +32,28 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
return tree;
|
||||
}
|
||||
|
||||
private static int GetBlockSize()
|
||||
{
|
||||
var blockSize = 4096;
|
||||
|
||||
var appSetting = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.BTree.BlockSize"];
|
||||
if (appSetting == null)
|
||||
return blockSize;
|
||||
|
||||
if (!int.TryParse(appSetting, out blockSize))
|
||||
throw new ConfigurationErrorsException($"Invalid block size value \"{appSetting}\": not a number.");
|
||||
|
||||
var bit = 0;
|
||||
for (var i = blockSize; i != 1; i >>= 1)
|
||||
bit++;
|
||||
if (1 << bit != blockSize)
|
||||
throw new ConfigurationErrorsException($"Invalid block size value \"{blockSize}\": must be a power of two.");
|
||||
if (blockSize < 512 || blockSize > 65536)
|
||||
throw new ConfigurationErrorsException($"Invalid block size value \"{blockSize}\": must be >= 512 and <= 65536.");
|
||||
|
||||
return blockSize;
|
||||
}
|
||||
|
||||
/*
|
||||
class ListOfIntSerializer : ISerializer<List<int>>
|
||||
{
|
||||
|
||||
@@ -330,14 +330,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
private void LockAndLoadContent(Action<IScope> action)
|
||||
{
|
||||
using (_contentStore.GetWriter(_scopeProvider))
|
||||
// first get a writer, then a scope
|
||||
// if there already is a scope, the writer will attach to it
|
||||
// otherwise, it will only exist here - cheap
|
||||
using (_contentStore.GetScopedWriteLock(_scopeProvider))
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
action(scope);
|
||||
scope.Complete();
|
||||
}
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
action(scope);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +356,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
_logger.Debug<PublishedSnapshotService>("Loading content from database...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
// IMPORTANT GetAllContentSources sorts kits by level
|
||||
var kits = _dataSource.GetAllContentSources(scope);
|
||||
_contentStore.SetAll(kits);
|
||||
sw.Stop();
|
||||
@@ -369,7 +371,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
_logger.Debug<PublishedSnapshotService>("Loading content from local db...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
var kits = _localContentDb.Select(x => x.Value).OrderBy(x => x.Node.Level);
|
||||
var kits = _localContentDb.Select(x => x.Value)
|
||||
.OrderBy(x => x.Node.Level); // IMPORTANT sort by level
|
||||
_contentStore.SetAll(kits);
|
||||
sw.Stop();
|
||||
_logger.Debug<PublishedSnapshotService>("Loaded content from local db ({Duration}ms)", sw.ElapsedMilliseconds);
|
||||
@@ -399,14 +402,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
private void LockAndLoadMedia(Action<IScope> action)
|
||||
{
|
||||
using (_mediaStore.GetWriter(_scopeProvider))
|
||||
// see note in LockAndLoadContent
|
||||
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
action(scope);
|
||||
scope.Complete();
|
||||
}
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
action(scope);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +424,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
_logger.Debug<PublishedSnapshotService>("Loading media from database...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
// IMPORTANT GetAllMediaSources sorts kits by level
|
||||
var kits = _dataSource.GetAllMediaSources(scope);
|
||||
_mediaStore.SetAll(kits);
|
||||
sw.Stop();
|
||||
@@ -436,7 +439,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
_logger.Debug<PublishedSnapshotService>("Loading media from local db...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
var kits = _localMediaDb.Select(x => x.Value);
|
||||
var kits = _localMediaDb.Select(x => x.Value)
|
||||
.OrderBy(x => x.Node.Level); // IMPORTANT sort by level
|
||||
_mediaStore.SetAll(kits);
|
||||
sw.Stop();
|
||||
_logger.Debug<PublishedSnapshotService>("Loaded media from local db ({Duration}ms)", sw.ElapsedMilliseconds);
|
||||
@@ -528,14 +532,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
private void LockAndLoadDomains()
|
||||
{
|
||||
using (_domainStore.GetWriter(_scopeProvider))
|
||||
// see note in LockAndLoadContent
|
||||
using (_domainStore.GetScopedWriteLock(_scopeProvider))
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.Domains);
|
||||
LoadDomainsLocked();
|
||||
scope.Complete();
|
||||
}
|
||||
scope.ReadLock(Constants.Locks.Domains);
|
||||
LoadDomainsLocked();
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,7 +590,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return;
|
||||
}
|
||||
|
||||
using (_contentStore.GetWriter(_scopeProvider))
|
||||
using (_contentStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(payloads, out bool draftChanged2, out bool publishedChanged2);
|
||||
draftChanged = draftChanged2;
|
||||
@@ -648,6 +651,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
{
|
||||
// ?? should we do some RV check here?
|
||||
// IMPORTANT GetbranchContentSources sorts kits by level
|
||||
var kits = _dataSource.GetBranchContentSources(scope, capture.Id);
|
||||
_contentStore.SetBranch(capture.Id, kits);
|
||||
}
|
||||
@@ -683,7 +687,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return;
|
||||
}
|
||||
|
||||
using (_mediaStore.GetWriter(_scopeProvider))
|
||||
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
NotifyLocked(payloads, out bool anythingChanged2);
|
||||
anythingChanged = anythingChanged2;
|
||||
@@ -739,6 +743,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
{
|
||||
// ?? should we do some RV check here?
|
||||
// IMPORTANT GetbranchContentSources sorts kits by level
|
||||
var kits = _dataSource.GetBranchMediaSources(scope, capture.Id);
|
||||
_mediaStore.SetBranch(capture.Id, kits);
|
||||
}
|
||||
@@ -804,7 +809,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
if (removedIds.Count == 0 && refreshedIds.Count == 0 && otherIds.Count == 0 && newIds.Count == 0) return;
|
||||
|
||||
using (store.GetWriter(_scopeProvider))
|
||||
using (store.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
// ReSharper disable AccessToModifiedClosure
|
||||
action(removedIds, refreshedIds, otherIds, newIds);
|
||||
@@ -825,8 +830,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
payload.Removed ? "Removed" : "Refreshed",
|
||||
payload.Id);
|
||||
|
||||
using (_contentStore.GetWriter(_scopeProvider))
|
||||
using (_mediaStore.GetWriter(_scopeProvider))
|
||||
using (_contentStore.GetScopedWriteLock(_scopeProvider))
|
||||
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
// TODO: need to add a datatype lock
|
||||
// this is triggering datatypes reload in the factory, and right after we create some
|
||||
@@ -858,7 +863,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
if (_isReady == false)
|
||||
return;
|
||||
|
||||
using (_domainStore.GetWriter(_scopeProvider))
|
||||
// see note in LockAndLoadContent
|
||||
using (_domainStore.GetScopedWriteLock(_scopeProvider))
|
||||
{
|
||||
foreach (var payload in payloads)
|
||||
{
|
||||
@@ -1375,19 +1381,24 @@ WHERE cmsContentNu.nodeId IN (
|
||||
long total;
|
||||
do
|
||||
{
|
||||
// the tree is locked, counting and comparing to total is safe
|
||||
var descendants = _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
|
||||
var items = new List<ContentNuDto>();
|
||||
var count = 0;
|
||||
foreach (var c in descendants)
|
||||
{
|
||||
// always the edited version
|
||||
items.Add(GetDto(c, false));
|
||||
|
||||
// and also the published version if it makes any sense
|
||||
if (c.Published)
|
||||
items.Add(GetDto(c, true));
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
db.BulkInsertRecords(items);
|
||||
processed += items.Count;
|
||||
processed += count;
|
||||
} while (processed < total);
|
||||
}
|
||||
|
||||
@@ -1442,10 +1453,11 @@ WHERE cmsContentNu.nodeId IN (
|
||||
long total;
|
||||
do
|
||||
{
|
||||
// the tree is locked, counting and comparing to total is safe
|
||||
var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
|
||||
var items = descendants.Select(m => GetDto(m, false)).ToArray();
|
||||
var items = descendants.Select(m => GetDto(m, false)).ToList();
|
||||
db.BulkInsertRecords(items);
|
||||
processed += items.Length;
|
||||
processed += items.Count;
|
||||
} while (processed < total);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Snap
|
||||
{
|
||||
internal class GenObj
|
||||
{
|
||||
public GenObj(long gen)
|
||||
{
|
||||
Gen = gen;
|
||||
WeakGenRef = new WeakReference(null);
|
||||
}
|
||||
|
||||
public GenRef GetGenRef()
|
||||
{
|
||||
// not thread-safe but always invoked from within a lock
|
||||
var genRef = (GenRef)WeakGenRef.Target;
|
||||
if (genRef == null)
|
||||
WeakGenRef.Target = genRef = new GenRef(this);
|
||||
return genRef;
|
||||
}
|
||||
|
||||
public readonly long Gen;
|
||||
public readonly WeakReference WeakGenRef;
|
||||
public int Count;
|
||||
|
||||
public void Reference()
|
||||
{
|
||||
Interlocked.Increment(ref Count);
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
Interlocked.Decrement(ref Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Snap
|
||||
{
|
||||
internal class GenRef
|
||||
{
|
||||
public GenRef(GenObj genObj)
|
||||
{
|
||||
GenObj = genObj;
|
||||
}
|
||||
|
||||
public readonly GenObj GenObj;
|
||||
public long Gen => GenObj.Gen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Snap
|
||||
{
|
||||
internal class LinkedNode<TValue>
|
||||
where TValue : class
|
||||
{
|
||||
public LinkedNode(TValue value, long gen, LinkedNode<TValue> next = null)
|
||||
{
|
||||
Value = value;
|
||||
Gen = gen;
|
||||
Next = next;
|
||||
}
|
||||
|
||||
public readonly long Gen;
|
||||
|
||||
// reading & writing references is thread-safe on all .NET platforms
|
||||
// mark as volatile to ensure we always read the correct value
|
||||
public volatile TValue Value;
|
||||
public volatile LinkedNode<TValue> Next;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Web.PublishedCache.NuCache.Snap;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
@@ -20,9 +21,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// This class is optimized for many readers, few writers
|
||||
// Readers are lock-free
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, LinkedNode> _items;
|
||||
private readonly ConcurrentQueue<GenerationObject> _generationObjects;
|
||||
private GenerationObject _generationObject;
|
||||
private readonly ConcurrentDictionary<TKey, LinkedNode<TValue>> _items;
|
||||
private readonly ConcurrentQueue<GenObj> _genObjs;
|
||||
private GenObj _genObj;
|
||||
private readonly object _wlocko = new object();
|
||||
private readonly object _rlocko = new object();
|
||||
private long _liveGen, _floorGen;
|
||||
@@ -40,9 +41,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
public SnapDictionary()
|
||||
{
|
||||
_items = new ConcurrentDictionary<TKey, LinkedNode>();
|
||||
_generationObjects = new ConcurrentQueue<GenerationObject>();
|
||||
_generationObject = null; // no initial gen exists
|
||||
_items = new ConcurrentDictionary<TKey, LinkedNode<TValue>>();
|
||||
_genObjs = new ConcurrentQueue<GenObj>();
|
||||
_genObj = null; // no initial gen exists
|
||||
_liveGen = _floorGen = 0;
|
||||
_nextGen = false; // first time, must create a snapshot
|
||||
_collectAuto = true; // collect automatically by default
|
||||
@@ -86,12 +87,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
|
||||
// a scope contextual that represents a locked writer to the dictionary
|
||||
private class SnapDictionaryWriter : ScopeContextualBase
|
||||
private class ScopedWriteLock : ScopeContextualBase
|
||||
{
|
||||
private readonly WriteLockInfo _lockinfo = new WriteLockInfo();
|
||||
private SnapDictionary<TKey, TValue> _dictionary;
|
||||
private readonly SnapDictionary<TKey, TValue> _dictionary;
|
||||
private int _released;
|
||||
|
||||
public SnapDictionaryWriter(SnapDictionary<TKey, TValue> dictionary, bool scoped)
|
||||
public ScopedWriteLock(SnapDictionary<TKey, TValue> dictionary, bool scoped)
|
||||
{
|
||||
_dictionary = dictionary;
|
||||
dictionary.Lock(_lockinfo, scoped);
|
||||
@@ -99,17 +101,19 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
public override void Release(bool completed)
|
||||
{
|
||||
if (_dictionary == null) return;
|
||||
if (Interlocked.CompareExchange(ref _released, 1, 0) != 0)
|
||||
return;
|
||||
_dictionary.Release(_lockinfo, completed);
|
||||
_dictionary = null;
|
||||
}
|
||||
}
|
||||
|
||||
// gets a scope contextual representing a locked writer to the dictionary
|
||||
// GetScopedWriter? should the dict have a ref onto the scope provider?
|
||||
public IDisposable GetWriter(IScopeProvider scopeProvider)
|
||||
// the dict is write-locked until the write-lock is released
|
||||
// which happens when it is disposed (non-scoped)
|
||||
// or when the scope context exits (scoped)
|
||||
public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider)
|
||||
{
|
||||
return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new SnapDictionaryWriter(this, scoped));
|
||||
return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped));
|
||||
}
|
||||
|
||||
private void Lock(WriteLockInfo lockInfo, bool forceGen = false)
|
||||
@@ -129,14 +133,18 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
//RuntimeHelpers.PrepareConstrainedRegions();
|
||||
try { } finally
|
||||
{
|
||||
// increment the lock count, and register that this lock is counting
|
||||
_wlocked++;
|
||||
lockInfo.Count = true;
|
||||
if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects
|
||||
|
||||
if (_nextGen == false || (forceGen && _wlocked == 1))
|
||||
{
|
||||
// because we are changing things, a new generation
|
||||
// is created, which will trigger a new snapshot
|
||||
_nextGen = true;
|
||||
if (_nextGen)
|
||||
_genObjs.Enqueue(_genObj = new GenObj(_liveGen));
|
||||
_liveGen += 1;
|
||||
_nextGen = true; // this is the ONLY place where _nextGen becomes true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +161,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
private void Release(WriteLockInfo lockInfo, bool commit = true)
|
||||
{
|
||||
// if the lock wasn't taken in the first place, do nothing
|
||||
if (!lockInfo.Taken)
|
||||
return;
|
||||
|
||||
if (commit == false)
|
||||
{
|
||||
var rtaken = false;
|
||||
@@ -161,6 +173,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
Monitor.Enter(_rlocko, ref rtaken);
|
||||
try { } finally
|
||||
{
|
||||
// forget about the temp. liveGen
|
||||
_nextGen = false;
|
||||
_liveGen -= 1;
|
||||
}
|
||||
@@ -183,8 +196,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
|
||||
// decrement the lock count, if counting, then exit the lock
|
||||
if (lockInfo.Count) _wlocked--;
|
||||
if (lockInfo.Taken) Monitor.Exit(_wlocko);
|
||||
Monitor.Exit(_wlocko);
|
||||
}
|
||||
|
||||
private void Release(ReadLockInfo lockInfo)
|
||||
@@ -198,9 +212,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
public int Count => _items.Count;
|
||||
|
||||
private LinkedNode GetHead(TKey key)
|
||||
private LinkedNode<TValue> GetHead(TKey key)
|
||||
{
|
||||
_items.TryGetValue(key, out LinkedNode link); // else null
|
||||
_items.TryGetValue(key, out var link); // else null
|
||||
return link;
|
||||
}
|
||||
|
||||
@@ -221,7 +235,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// for an older gen - if value is different then insert a new
|
||||
// link for the new gen, with the new value
|
||||
if (link.Value != value)
|
||||
_items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link);
|
||||
_items.TryUpdate(key, new LinkedNode<TValue>(value, _liveGen, link), link);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -235,7 +249,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
else
|
||||
{
|
||||
_items.TryAdd(key, new LinkedNode(value, _liveGen));
|
||||
_items.TryAdd(key, new LinkedNode<TValue>(value, _liveGen));
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -261,7 +275,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
if (kvp.Value.Gen < _liveGen)
|
||||
{
|
||||
var link = new LinkedNode(null, _liveGen, kvp.Value);
|
||||
var link = new LinkedNode<TValue>(null, _liveGen, kvp.Value);
|
||||
_items.TryUpdate(kvp.Key, link, kvp.Value);
|
||||
}
|
||||
else
|
||||
@@ -337,12 +351,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
Lock(lockInfo);
|
||||
|
||||
// if no next generation is required, and we already have one,
|
||||
// use it and create a new snapshot
|
||||
if (_nextGen == false && _generationObject != null)
|
||||
return new Snapshot(this, _generationObject.GetReference());
|
||||
// if no next generation is required, and we already have a gen object,
|
||||
// use it to create a new snapshot
|
||||
if (_nextGen == false && _genObj != null)
|
||||
return new Snapshot(this, _genObj.GetGenRef());
|
||||
|
||||
// else we need to try to create a new gen ref
|
||||
// else we need to try to create a new gen object
|
||||
// whether we are wlocked or not, noone can rlock while we do,
|
||||
// so _liveGen and _nextGen are safe
|
||||
if (_wlocked > 0) // volatile, cannot ++ but could --
|
||||
@@ -350,29 +364,32 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// write-locked, cannot use latest gen (at least 1) so use previous
|
||||
var snapGen = _nextGen ? _liveGen - 1 : _liveGen;
|
||||
|
||||
// create a new gen ref unless we already have it
|
||||
if (_generationObject == null)
|
||||
_generationObjects.Enqueue(_generationObject = new GenerationObject(snapGen));
|
||||
else if (_generationObject.Gen != snapGen)
|
||||
// create a new gen object if we don't already have one
|
||||
// (happens the first time a snapshot is created)
|
||||
if (_genObj == null)
|
||||
_genObjs.Enqueue(_genObj = new GenObj(snapGen));
|
||||
|
||||
// if we have one already, ensure it's consistent
|
||||
else if (_genObj.Gen != snapGen)
|
||||
throw new Exception("panic");
|
||||
}
|
||||
else
|
||||
{
|
||||
// not write-locked, can use latest gen, create a new gen ref
|
||||
_generationObjects.Enqueue(_generationObject = new GenerationObject(_liveGen));
|
||||
// not write-locked, can use latest gen (_liveGen), create a corresponding new gen object
|
||||
_genObjs.Enqueue(_genObj = new GenObj(_liveGen));
|
||||
_nextGen = false; // this is the ONLY thing that triggers a _liveGen++
|
||||
}
|
||||
|
||||
// so...
|
||||
// the genRefRef has a weak ref to the genRef, and is queued
|
||||
// the snapshot has a ref to the genRef, which has a ref to the genRefRef
|
||||
// when the snapshot is disposed, it decreases genRefRef counter
|
||||
// the genObj has a weak ref to the genRef, and is queued
|
||||
// the snapshot has a ref to the genRef, which has a ref to the genObj
|
||||
// when the snapshot is disposed, it decreases genObj counter
|
||||
// so after a while, one of these conditions is going to be true:
|
||||
// - the genRefRef counter is zero because all snapshots have properly been disposed
|
||||
// - the genRefRef weak ref is dead because all snapshots have been collected
|
||||
// - genObj.Count is zero because all snapshots have properly been disposed
|
||||
// - genObj.WeakGenRef is dead because all snapshots have been collected
|
||||
// in both cases, we will dequeue and collect
|
||||
|
||||
var snapshot = new Snapshot(this, _generationObject.GetReference());
|
||||
var snapshot = new Snapshot(this, _genObj.GetGenRef());
|
||||
|
||||
// reading _floorGen is safe if _collectTask is null
|
||||
if (_collectTask == null && _collectAuto && _liveGen - _floorGen > CollectMinGenDelta)
|
||||
@@ -416,16 +433,16 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
private void Collect()
|
||||
{
|
||||
// see notes in CreateSnapshot
|
||||
while (_generationObjects.TryPeek(out GenerationObject generationObject) && (generationObject.Count == 0 || generationObject.WeakReference.IsAlive == false))
|
||||
while (_genObjs.TryPeek(out var genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false))
|
||||
{
|
||||
_generationObjects.TryDequeue(out generationObject); // cannot fail since TryPeek has succeeded
|
||||
_floorGen = generationObject.Gen;
|
||||
_genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded
|
||||
_floorGen = genObj.Gen;
|
||||
}
|
||||
|
||||
Collect(_items);
|
||||
}
|
||||
|
||||
private void Collect(ConcurrentDictionary<TKey, LinkedNode> dict)
|
||||
private void Collect(ConcurrentDictionary<TKey, LinkedNode<TValue>> dict)
|
||||
{
|
||||
// it is OK to enumerate a concurrent dictionary and it does not lock
|
||||
// it - and here it's not an issue if we skip some items, they will be
|
||||
@@ -460,7 +477,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// not live, null value, no next link = remove that one -- but only if
|
||||
// the dict has not been updated, have to do it via ICollection<> (thanks
|
||||
// Mr Toub) -- and if the dict has been updated there is nothing to collect
|
||||
var idict = dict as ICollection<KeyValuePair<TKey, LinkedNode>>;
|
||||
var idict = dict as ICollection<KeyValuePair<TKey, LinkedNode<TValue>>>;
|
||||
/*var removed =*/ idict.Remove(kvp);
|
||||
//Console.WriteLine("remove (" + (removed ? "true" : "false") + ")");
|
||||
continue;
|
||||
@@ -485,14 +502,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
task = _collectTask;
|
||||
}
|
||||
return task ?? Task.FromResult(0);
|
||||
return task ?? Task.CompletedTask;
|
||||
//if (task != null)
|
||||
// await task;
|
||||
}
|
||||
|
||||
public long GenCount => _generationObjects.Count;
|
||||
public long GenCount => _genObjs.Count;
|
||||
|
||||
public long SnapCount => _generationObjects.Sum(x => x.Count);
|
||||
public long SnapCount => _genObjs.Sum(x => x.Count);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -513,6 +530,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
public long LiveGen => _dict._liveGen;
|
||||
public long FloorGen => _dict._floorGen;
|
||||
public bool NextGen => _dict._nextGen;
|
||||
public int WLocked => _dict._wlocked;
|
||||
|
||||
public bool CollectAuto
|
||||
{
|
||||
@@ -520,13 +538,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
set => _dict._collectAuto = value;
|
||||
}
|
||||
|
||||
public ConcurrentQueue<GenerationObject> GenerationObjects => _dict._generationObjects;
|
||||
public GenObj GenObj => _dict._genObj;
|
||||
|
||||
public ConcurrentQueue<GenObj> GenObjs => _dict._genObjs;
|
||||
|
||||
public Snapshot LiveSnapshot => new Snapshot(_dict, _dict._liveGen);
|
||||
|
||||
public GenVal[] GetValues(TKey key)
|
||||
{
|
||||
_dict._items.TryGetValue(key, out LinkedNode link); // else null
|
||||
_dict._items.TryGetValue(key, out var link); // else null
|
||||
|
||||
if (link == null)
|
||||
return new GenVal[0];
|
||||
@@ -559,35 +579,23 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
#region Classes
|
||||
|
||||
private class LinkedNode
|
||||
{
|
||||
public LinkedNode(TValue value, long gen, LinkedNode next = null)
|
||||
{
|
||||
Value = value;
|
||||
Gen = gen;
|
||||
Next = next;
|
||||
}
|
||||
|
||||
internal readonly long Gen;
|
||||
|
||||
// reading & writing references is thread-safe on all .NET platforms
|
||||
// mark as volatile to ensure we always read the correct value
|
||||
internal volatile TValue Value;
|
||||
internal volatile LinkedNode Next;
|
||||
}
|
||||
|
||||
public class Snapshot : IDisposable
|
||||
{
|
||||
private readonly SnapDictionary<TKey, TValue> _store;
|
||||
private readonly GenerationReference _generationReference;
|
||||
private long _gen; // copied for perfs
|
||||
private readonly GenRef _genRef;
|
||||
private readonly long _gen; // copied for perfs
|
||||
private int _disposed;
|
||||
|
||||
internal Snapshot(SnapDictionary<TKey, TValue> store, GenerationReference generationReference)
|
||||
//private static int _count;
|
||||
//private readonly int _thisCount;
|
||||
|
||||
internal Snapshot(SnapDictionary<TKey, TValue> store, GenRef genRef)
|
||||
{
|
||||
_store = store;
|
||||
_generationReference = generationReference;
|
||||
_gen = generationReference.GenerationObject.Gen;
|
||||
_generationReference.GenerationObject.Reference();
|
||||
_genRef = genRef;
|
||||
_gen = genRef.GenObj.Gen;
|
||||
_genRef.GenObj.Reference();
|
||||
//_thisCount = _count++;
|
||||
}
|
||||
|
||||
internal Snapshot(SnapDictionary<TKey, TValue> store, long gen)
|
||||
@@ -596,17 +604,21 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
_gen = gen;
|
||||
}
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
{
|
||||
if (_disposed > 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
}
|
||||
|
||||
public TValue Get(TKey key)
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
EnsureNotDisposed();
|
||||
return _store.Get(key, _gen);
|
||||
}
|
||||
|
||||
public IEnumerable<TValue> GetAll()
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
EnsureNotDisposed();
|
||||
return _store.GetAll(_gen);
|
||||
}
|
||||
|
||||
@@ -614,8 +626,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
EnsureNotDisposed();
|
||||
return _store.IsEmpty(_gen);
|
||||
}
|
||||
}
|
||||
@@ -624,63 +635,20 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
EnsureNotDisposed();
|
||||
return _gen;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_gen < 0) return;
|
||||
_gen = -1;
|
||||
_generationReference?.GenerationObject.Release();
|
||||
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
|
||||
return;
|
||||
_genRef?.GenObj.Release();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal class GenerationObject
|
||||
{
|
||||
public GenerationObject(long gen)
|
||||
{
|
||||
Gen = gen;
|
||||
WeakReference = new WeakReference(null);
|
||||
}
|
||||
|
||||
public GenerationReference GetReference()
|
||||
{
|
||||
// not thread-safe but always invoked from within a lock
|
||||
var generationReference = (GenerationReference) WeakReference.Target;
|
||||
if (generationReference == null)
|
||||
WeakReference.Target = generationReference = new GenerationReference(this);
|
||||
return generationReference;
|
||||
}
|
||||
|
||||
public readonly long Gen;
|
||||
public readonly WeakReference WeakReference;
|
||||
public int Count;
|
||||
|
||||
public void Reference()
|
||||
{
|
||||
Interlocked.Increment(ref Count);
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
Interlocked.Decrement(ref Count);
|
||||
}
|
||||
}
|
||||
|
||||
internal class GenerationReference
|
||||
{
|
||||
public GenerationReference(GenerationObject generationObject)
|
||||
{
|
||||
GenerationObject = generationObject;
|
||||
}
|
||||
|
||||
public readonly GenerationObject GenerationObject;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,5 +170,23 @@ namespace Umbraco.Web
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsSomething
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the content is visible.
|
||||
/// </summary>
|
||||
/// <param name="content">The content.</param>
|
||||
/// <returns>A value indicating whether the content is visible.</returns>
|
||||
/// <remarks>A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise,
|
||||
/// the content is visible.</remarks>
|
||||
public static bool IsVisible(this IPublishedElement content)
|
||||
{
|
||||
// rely on the property converter - will return default bool value, ie false, if property
|
||||
// is not defined, or has no value, else will return its value.
|
||||
return content.Value<bool>(Constants.Conventions.Content.NaviHide) == false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace Umbraco.Web.Runtime
|
||||
{
|
||||
// web's final composer composes after all user composers
|
||||
// and *also* after ICoreComposer (in case IUserComposer is disabled)
|
||||
[ComposeAfter(typeof(IUserComposer))]
|
||||
[ComposeAfter(typeof(ICoreComposer))]
|
||||
public class WebFinalComposer : ComponentComposer<WebFinalComponent>
|
||||
{ }
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ namespace Umbraco.Web.Trees
|
||||
/// <returns></returns>
|
||||
protected bool IsDialog(FormDataCollection queryStrings)
|
||||
{
|
||||
return queryStrings.GetValue<bool>(TreeQueryStringParameters.IsDialog);
|
||||
return queryStrings.GetValue<string>(TreeQueryStringParameters.Use) == "dialog";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/// </summary>
|
||||
internal struct TreeQueryStringParameters
|
||||
{
|
||||
public const string IsDialog = "isDialog";
|
||||
public const string Use = "use";
|
||||
public const string Application = "application";
|
||||
public const string StartNodeId = "startNodeId";
|
||||
//public const string OnNodeClick = "OnNodeClick";
|
||||
|
||||
@@ -205,6 +205,9 @@
|
||||
<Compile Include="Models\PublishedContent\HybridVariationContextAccessor.cs" />
|
||||
<Compile Include="Models\TemplateQuery\QueryConditionExtensions.cs" />
|
||||
<Compile Include="Mvc\SurfaceControllerTypeCollectionBuilder.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Snap\GenObj.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Snap\GenRef.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Snap\LinkedNode.cs" />
|
||||
<Compile Include="Routing\IPublishedRouter.cs" />
|
||||
<Compile Include="Services\DashboardService.cs" />
|
||||
<Compile Include="Services\IDashboardService.cs" />
|
||||
|
||||
Reference in New Issue
Block a user