diff --git a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs new file mode 100644 index 0000000000..eb9db80990 --- /dev/null +++ b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs @@ -0,0 +1,43 @@ +using System; + +namespace Umbraco.Core.Collections +{ + /// + /// Represents a composite key of (int, string) for fast dictionaries. + /// + /// + /// The integer part of the key must be greater than, or equal to, zero. + /// The string part of the key is case-insensitive. + /// Null is a valid value for both parts. + /// + public struct CompositeIntStringKey : IEquatable + { + private readonly int _key1; + private readonly string _key2; + + /// + /// Initializes a new instance of the struct. + /// + public CompositeIntStringKey(int? key1, string key2) + { + if (key1 < 0) throw new ArgumentOutOfRangeException(nameof(key1)); + _key1 = key1 ?? -1; + _key2 = key2?.ToLowerInvariant() ?? "NULL"; + } + + public bool Equals(CompositeIntStringKey other) + => _key2 == other._key2 && _key1 == other._key1; + + public override bool Equals(object obj) + => obj is CompositeIntStringKey other && _key2 == other._key2 && _key1 == other._key1; + + public override int GetHashCode() + => _key2.GetHashCode() * 31 + _key1; + + public static bool operator ==(CompositeIntStringKey key1, CompositeIntStringKey key2) + => key1._key2 == key2._key2 && key1._key1 == key2._key1; + + public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) + => key1._key2 != key2._key2 || key1._key1 != key2._key1; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 6805eb3504..0dad772a91 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -339,6 +339,9 @@ namespace Umbraco.Core.Models var published = CopyingFromSelf(other); + // segment is invariant in comparisons + segment = segment?.ToLowerInvariant(); + // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails // clear all existing properties diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index d90084f60b..0d8ab300c8 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using Umbraco.Core.Collections; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Models @@ -15,12 +16,11 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class Property : Entity { - private PropertyType _propertyType; private List _tagChanges; private List _values = new List(); private PropertyValue _pvalue; - private Dictionary _vvalues; + private Dictionary _vvalues; private static readonly Lazy Ps = new Lazy(); @@ -29,51 +29,30 @@ namespace Umbraco.Core.Models public Property(PropertyType propertyType) { - _propertyType = propertyType; + PropertyType = propertyType; } public Property(int id, PropertyType propertyType) { Id = id; - _propertyType = propertyType; + PropertyType = propertyType; } public class PropertyValue { + private string _segment; + public int? LanguageId { get; internal set; } - public string Segment { get; internal set; } + public string Segment + { + get => _segment; + internal set => _segment = value?.ToLowerInvariant(); + } public object EditedValue { get; internal set; } public object PublishedValue { get; internal set; } public PropertyValue Clone() - => new PropertyValue { LanguageId = LanguageId, Segment = Segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; - } - - private struct CompositeKey : IEquatable - { - private readonly int _key1; - private readonly string _key2; - - public CompositeKey(int? key1, string key2) - { - _key1 = key1 ?? -1; - _key2 = key2?.ToLowerInvariant() ?? "NEUTRAL"; - } - - public bool Equals(CompositeKey other) - => _key2 == other._key2 && _key1 == other._key1; - - public override bool Equals(object obj) - => obj is CompositeKey other && _key2 == other._key2 && _key1 == other._key1; - - public override int GetHashCode() - => _key2.GetHashCode() * 31 + _key1; - - public static bool operator ==(CompositeKey key1, CompositeKey key2) - => key1._key2 == key2._key2 && key1._key1 == key2._key1; - - public static bool operator !=(CompositeKey key1, CompositeKey key2) - => key1._key2 != key2._key2 || key1._key1 != key2._key1; + => new PropertyValue { LanguageId = LanguageId, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; } // ReSharper disable once ClassNeverInstantiated.Local @@ -106,7 +85,7 @@ namespace Umbraco.Core.Models /// Returns the PropertyType, which this Property is based on /// [IgnoreDataMember] - public PropertyType PropertyType => _propertyType; + public PropertyType PropertyType { get; private set; } /// /// Gets the list of values. @@ -119,10 +98,10 @@ namespace Umbraco.Core.Models { // make sure we filter out invalid variations // make sure we leave _vvalues null if possible - _values = value.Where(x => _propertyType.ValidateVariation(x.LanguageId, x.Segment, false)).ToList(); + _values = value.Where(x => PropertyType.ValidateVariation(x.LanguageId, x.Segment, false)).ToList(); _pvalue = _values.FirstOrDefault(x => !x.LanguageId.HasValue && x.Segment == null); _vvalues = _values.Count > (_pvalue == null ? 0 : 1) - ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeKey(x.LanguageId, x.Segment), x => x) + ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeIntStringKey(x.LanguageId, x.Segment), x => x) : null; } } @@ -141,13 +120,13 @@ namespace Umbraco.Core.Models /// Returns the Alias of the PropertyType, which this Property is based on /// [DataMember] - public string Alias => _propertyType.Alias; + public string Alias => PropertyType.Alias; /// /// Returns the Id of the PropertyType, which this Property is based on /// [IgnoreDataMember] - internal int PropertyTypeId => _propertyType.Id; + internal int PropertyTypeId => PropertyType.Id; /// /// Returns the DatabaseType that the underlaying DataType is using to store its values @@ -156,17 +135,17 @@ namespace Umbraco.Core.Models /// Only used internally when saving the property value. /// [IgnoreDataMember] - internal DataTypeDatabaseType DataTypeDatabaseType => _propertyType.DataTypeDatabaseType; + internal DataTypeDatabaseType DataTypeDatabaseType => PropertyType.DataTypeDatabaseType; /// /// Gets the value. /// public object GetValue(int? languageId = null, string segment = null, bool published = false) { - if (!_propertyType.ValidateVariation(languageId, segment, false)) return null; + if (!PropertyType.ValidateVariation(languageId, segment, false)) return null; if (!languageId.HasValue && segment == null) return GetPropertyValue(_pvalue, published); if (_vvalues == null) return null; - return _vvalues.TryGetValue(new CompositeKey(languageId, segment), out var pvalue) + return _vvalues.TryGetValue(new CompositeIntStringKey(languageId, segment), out var pvalue) ? GetPropertyValue(pvalue, published) : null; } @@ -175,7 +154,7 @@ namespace Umbraco.Core.Models { if (pvalue == null) return null; - return _propertyType.IsPublishing + return PropertyType.IsPublishing ? (published ? pvalue.PublishedValue : pvalue.EditedValue) : pvalue.EditedValue; } @@ -185,14 +164,14 @@ namespace Umbraco.Core.Models internal void PublishAllValues() { // if invariant-neutral is supported, publish invariant-neutral - if (_propertyType.ValidateVariation(null, null, false)) + if (PropertyType.ValidateVariation(null, null, false)) PublishPropertyValue(_pvalue); // publish everything not invariant-neutral that is supported if (_vvalues != null) { var pvalues = _vvalues - .Where(x => _propertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) PublishPropertyValue(pvalue); @@ -203,7 +182,7 @@ namespace Umbraco.Core.Models // does *not* validate the value - content item must validate first internal void PublishValue(int? languageId = null, string segment = null) { - _propertyType.ValidateVariation(languageId, segment, true); + PropertyType.ValidateVariation(languageId, segment, true); (var pvalue, _) = GetPValue(languageId, segment, false); if (pvalue == null) return; @@ -215,7 +194,7 @@ namespace Umbraco.Core.Models internal void PublishCultureValues(int? languageId = null) { // if invariant and invariant-neutral is supported, publish invariant-neutral - if (!languageId.HasValue && _propertyType.ValidateVariation(null, null, false)) + if (!languageId.HasValue && PropertyType.ValidateVariation(null, null, false)) PublishPropertyValue(_pvalue); // publish everything not invariant-neutral that matches the culture and is supported @@ -223,7 +202,7 @@ namespace Umbraco.Core.Models { var pvalues = _vvalues .Where(x => x.Value.LanguageId == languageId) - .Where(x => _propertyType.ValidateVariation(languageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(languageId, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) PublishPropertyValue(pvalue); @@ -233,13 +212,13 @@ namespace Umbraco.Core.Models // internal - must be invoked by the content item internal void ClearPublishedAllValues() { - if (_propertyType.ValidateVariation(null, null, false)) + if (PropertyType.ValidateVariation(null, null, false)) ClearPublishedPropertyValue(_pvalue); if (_vvalues != null) { var pvalues = _vvalues - .Where(x => _propertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) ClearPublishedPropertyValue(pvalue); @@ -249,7 +228,7 @@ namespace Umbraco.Core.Models // internal - must be invoked by the content item internal void ClearPublishedValue(int? languageId = null, string segment = null) { - _propertyType.ValidateVariation(languageId, segment, true); + PropertyType.ValidateVariation(languageId, segment, true); (var pvalue, _) = GetPValue(languageId, segment, false); if (pvalue == null) return; ClearPublishedPropertyValue(pvalue); @@ -258,14 +237,14 @@ namespace Umbraco.Core.Models // internal - must be invoked by the content item internal void ClearPublishedCultureValues(int? languageId = null) { - if (!languageId.HasValue && _propertyType.ValidateVariation(null, null, false)) + if (!languageId.HasValue && PropertyType.ValidateVariation(null, null, false)) ClearPublishedPropertyValue(_pvalue); if (_vvalues != null) { var pvalues = _vvalues .Where(x => x.Value.LanguageId == languageId) - .Where(x => _propertyType.ValidateVariation(languageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(languageId, x.Value.Segment, false)) .Select(x => x.Value); foreach (var pvalue in pvalues) ClearPublishedPropertyValue(pvalue); @@ -276,7 +255,7 @@ namespace Umbraco.Core.Models { if (pvalue == null) return; - if (!_propertyType.IsPublishing) + if (!PropertyType.IsPublishing) throw new NotSupportedException("Property type does not support publishing."); var origValue = pvalue.PublishedValue; pvalue.PublishedValue = ConvertSetValue(pvalue.EditedValue); @@ -287,7 +266,7 @@ namespace Umbraco.Core.Models { if (pvalue == null) return; - if (!_propertyType.IsPublishing) + if (!PropertyType.IsPublishing) throw new NotSupportedException("Property type does not support publishing."); var origValue = pvalue.PublishedValue; pvalue.PublishedValue = ConvertSetValue(null); @@ -299,7 +278,7 @@ namespace Umbraco.Core.Models /// public void SetValue(object value, int? languageId = null, string segment = null) { - _propertyType.ValidateVariation(languageId, segment, true); + PropertyType.ValidateVariation(languageId, segment, true); (var pvalue, var change) = GetPValue(languageId, segment, true); var origValue = pvalue.EditedValue; @@ -315,7 +294,7 @@ namespace Umbraco.Core.Models { (var pvalue, _) = GetPValue(languageId, segment, true); - if (published && _propertyType.IsPublishing) + if (published && PropertyType.IsPublishing) pvalue.PublishedValue = value; else pvalue.EditedValue = value; @@ -343,10 +322,10 @@ namespace Umbraco.Core.Models if (_vvalues == null) { if (!create) return (null, false); - _vvalues = new Dictionary(); + _vvalues = new Dictionary(); change = true; } - var k = new CompositeKey(languageId, segment); + var k = new CompositeIntStringKey(languageId, segment); if (!_vvalues.TryGetValue(k, out var pvalue)) { if (!create) return (null, false); @@ -361,7 +340,7 @@ namespace Umbraco.Core.Models private object ConvertSetValue(object value) { - var isOfExpectedType = _propertyType.IsPropertyTypeValid(value); + var isOfExpectedType = PropertyType.IsPropertyTypeValid(value); if (isOfExpectedType) return value; @@ -372,7 +351,7 @@ namespace Umbraco.Core.Models var s = value.ToString(); - switch (_propertyType.DataTypeDatabaseType) + switch (PropertyType.DataTypeDatabaseType) { case DataTypeDatabaseType.Nvarchar: case DataTypeDatabaseType.Ntext: @@ -382,14 +361,14 @@ namespace Umbraco.Core.Models if (s.IsNullOrWhiteSpace()) return null; // assume empty means null var convInt = value.TryConvertTo(); - if (convInt == false) ThrowTypeException(value, typeof(int), _propertyType.Alias); + if (convInt == false) ThrowTypeException(value, typeof(int), PropertyType.Alias); return convInt.Result; case DataTypeDatabaseType.Decimal: if (s.IsNullOrWhiteSpace()) return null; // assume empty means null var convDecimal = value.TryConvertTo(); - if (convDecimal == false) ThrowTypeException(value, typeof(decimal), _propertyType.Alias); + if (convDecimal == false) ThrowTypeException(value, typeof(decimal), PropertyType.Alias); // need to normalize the value (change the scaling factor and remove trailing zeroes) // because the underlying database is going to mess with the scaling factor anyways. return convDecimal.Result.Normalize(); @@ -398,7 +377,7 @@ namespace Umbraco.Core.Models if (s.IsNullOrWhiteSpace()) return null; // assume empty means null var convDateTime = value.TryConvertTo(); - if (convDateTime == false) ThrowTypeException(value, typeof(DateTime), _propertyType.Alias); + if (convDateTime == false) ThrowTypeException(value, typeof(DateTime), PropertyType.Alias); return convDateTime.Result; } @@ -418,7 +397,7 @@ namespace Umbraco.Core.Models { // invariant-neutral is supported, validate invariant-neutral // includes mandatory validation - if (_propertyType.ValidateVariation(null, null, false) && !IsValidValue(_pvalue)) return false; + if (PropertyType.ValidateVariation(null, null, false) && !IsValidValue(_pvalue)) return false; // either invariant-neutral is not supported, or it is valid // for anything else, validate the existing values (including mandatory), @@ -427,7 +406,7 @@ namespace Umbraco.Core.Models if (_vvalues == null) return true; var pvalues = _vvalues - .Where(x => _propertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(x.Value.LanguageId, x.Value.Segment, false)) .Select(x => x.Value) .ToArray(); @@ -442,7 +421,7 @@ namespace Umbraco.Core.Models { // culture-neutral is supported, validate culture-neutral // includes mandatory validation - if (_propertyType.ValidateVariation(languageId, null, false) && !IsValidValue(GetValue(languageId))) + if (PropertyType.ValidateVariation(languageId, null, false) && !IsValidValue(GetValue(languageId))) return false; // either culture-neutral is not supported, or it is valid @@ -453,7 +432,7 @@ namespace Umbraco.Core.Models var pvalues = _vvalues .Where(x => x.Value.LanguageId == languageId) - .Where(x => _propertyType.ValidateVariation(languageId, x.Value.Segment, false)) + .Where(x => PropertyType.ValidateVariation(languageId, x.Value.Segment, false)) .Select(x => x.Value) .ToArray(); @@ -477,7 +456,7 @@ namespace Umbraco.Core.Models /// True is property value is valid, otherwise false private bool IsValidValue(object value) { - return _propertyType.IsValidPropertyValue(value); + return PropertyType.IsValidPropertyValue(value); } public override object DeepClone() @@ -488,7 +467,7 @@ namespace Umbraco.Core.Models clone.DisableChangeTracking(); //need to manually assign since this is a readonly property - clone._propertyType = (PropertyType) PropertyType.DeepClone(); + clone.PropertyType = (PropertyType) PropertyType.DeepClone(); //re-enable tracking clone.ResetDirtyProperties(false); // not needed really, since we're not tracking diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 405687cf68..448a91238b 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1148,6 +1148,8 @@ namespace Umbraco.Core.Services /// public IEnumerable SaveAndPublishBranch(IContent content, bool force, int? languageId = null, string segment = null, int userId = 0) { + segment = segment?.ToLowerInvariant(); + bool IsEditing(IContent c, int? l, string s) => c.Properties.Any(x => x.Values.Where(y => y.LanguageId == l && y.Segment == s).Any(y => y.EditedValue != y.PublishedValue)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8e5b4170eb..ed61dd874d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -139,6 +139,7 @@ + diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 62eb5f3222..c74e115df9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Serialization; using Umbraco.Core.Cache; +using Umbraco.Core.Collections; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -21,12 +22,15 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly object _locko = new object(); + // the invariant-neutral source and inter values private readonly object _sourceValue; private bool _interInitialized; private object _interValue; - private List _sourceVValues; + // the variant source and inter values + private Dictionary _sourceValues; + // the variant and non-variant object values private CacheValues _cacheValues; private string _valuesCacheKey; @@ -51,9 +55,10 @@ namespace Umbraco.Web.PublishedCache.NuCache } else { - if (_sourceVValues == null) - _sourceVValues = new List(); - _sourceVValues.Add(new PropertyValue { LanguageId = sourceValue.LanguageId, Segment = sourceValue.Segment, SourceValue = sourceValue.Value }); + if (_sourceValues == null) + _sourceValues = new Dictionary(); + _sourceValues[new CompositeIntStringKey(sourceValue.LanguageId, sourceValue.Segment)] + = new SourceInterValue { LanguageId = sourceValue.LanguageId, Segment = sourceValue.Segment, SourceValue = sourceValue.Value }; } } } @@ -70,7 +75,7 @@ namespace Umbraco.Web.PublishedCache.NuCache : base(origin.PropertyType, origin.ReferenceCacheLevel) { _sourceValue = origin._sourceValue; - _sourceVValues = origin._sourceVValues; + _sourceValues = origin._sourceValues; _contentUid = origin._contentUid; _content = content; @@ -143,22 +148,19 @@ namespace Umbraco.Web.PublishedCache.NuCache if (languageId == null && segment == null) { if (_interInitialized) return _interValue; - _interValue = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing); _interInitialized = true; return _interValue; } - if (_sourceVValues == null) - _sourceVValues = new List(); + if (_sourceValues == null) + _sourceValues = new Dictionary(); - var vvalue = _sourceVValues.FirstOrDefault(x => x.LanguageId == languageId && x.Segment == segment); - - if (vvalue == null) - _sourceVValues.Add(vvalue = new PropertyValue { LanguageId = languageId, Segment = segment }); + var k = new CompositeIntStringKey(languageId, segment); + if (!_sourceValues.TryGetValue(k, out var vvalue)) + _sourceValues[k] = vvalue = new SourceInterValue { LanguageId = languageId, Segment = segment }; if (vvalue.InterInitialized) return vvalue.InterValue; - vvalue.InterValue = PropertyType.ConvertSourceToInter(_content, vvalue.SourceValue, _isPreviewing); vvalue.InterInitialized = true; return vvalue.InterValue; @@ -169,13 +171,11 @@ namespace Umbraco.Web.PublishedCache.NuCache if (languageId == null && segment == null) return _sourceValue; - List vvalues; lock (_locko) { - vvalues = _sourceVValues; + if (_sourceValues == null) return null; + return _sourceValues.TryGetValue(new CompositeIntStringKey(languageId, segment), out var sourceValue) ? sourceValue.SourceValue : null; } - - return vvalues?.FirstOrDefault(x => x.LanguageId == languageId && x.Segment == segment); } public override object GetValue(int? languageId = null, string segment = null) @@ -183,10 +183,12 @@ namespace Umbraco.Web.PublishedCache.NuCache lock (_locko) { var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(languageId, segment); - if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; // initial reference cache level always is .Content - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, PropertyCacheLevel.Element, GetInterValue(languageId, segment), _isPreviewing); + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; + cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(languageId, segment), _isPreviewing); cacheValues.ObjectInitialized = true; return cacheValues.ObjectValue; } @@ -197,10 +199,12 @@ namespace Umbraco.Web.PublishedCache.NuCache lock (_locko) { var cacheValues = GetCacheValues(PropertyType.CacheLevel).For(languageId, segment); - if (cacheValues.XPathInitialized) return cacheValues.XPathValue; // initial reference cache level always is .Content - cacheValues.XPathValue = PropertyType.ConvertInterToXPath(_content, PropertyCacheLevel.Element, GetInterValue(languageId, segment), _isPreviewing); + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + if (cacheValues.XPathInitialized) return cacheValues.XPathValue; + cacheValues.XPathValue = PropertyType.ConvertInterToXPath(_content, initialCacheLevel, GetInterValue(languageId, segment), _isPreviewing); cacheValues.XPathInitialized = true; return cacheValues.XPathValue; } @@ -208,40 +212,45 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Classes - private class CacheValues + private class CacheValue { - private int? _languageId; - private string _segment; + public bool ObjectInitialized { get; set; } + public object ObjectValue { get; set; } + public bool XPathInitialized { get; set; } + public object XPathValue { get; set; } + } - public bool ObjectInitialized; - public object ObjectValue; - public bool XPathInitialized; - public object XPathValue; - - private List _values; + private class CacheValues : CacheValue + { + private Dictionary _values; // this is always invoked from within a lock, so does not require its own lock - public CacheValues For(int? languageId, string segment) + public CacheValue For(int? languageId, string segment) { if (languageId == null && segment == null) return this; if (_values == null) - _values = new List(); + _values = new Dictionary(); - var values = _values.FirstOrDefault(x => x._languageId == languageId && x._segment == segment); + var k = new CompositeIntStringKey(languageId, segment); + if (!_values.TryGetValue(k, out var value)) + _values[k] = value = new CacheValue(); - if (values == null) - _values.Add(values = new CacheValues { _languageId = languageId, _segment = segment }); - - return values; + return value; } } - private class PropertyValue + private class SourceInterValue { + private string _segment; + public int? LanguageId { get; set; } - public string Segment { get; set; } + public string Segment + { + get => _segment; + internal set => _segment = value?.ToLowerInvariant(); + } public object SourceValue { get; set; } public bool InterInitialized { get; set; } public object InterValue { get; set; }