diff --git a/src/Umbraco.Core/Constants-Content.cs b/src/Umbraco.Core/Constants-Content.cs
deleted file mode 100644
index 3f12ece6dc..0000000000
--- a/src/Umbraco.Core/Constants-Content.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-namespace Umbraco.Core
-{
- public static partial class Constants
- {
- ///
- /// Defines content retrieval related constants
- ///
- public static class Content
- {
- ///
- /// Defines core supported content fall-back options when retrieving content property values.
- /// Defined as constants rather than enum to allow solution or package defined fall-back methods.
- ///
- public static class ValueFallback
- {
- ///
- /// No fallback at all.
- ///
- public const int None = -1;
-
- ///
- /// Default fallback.
- ///
- public const int Default = 0;
-
- ///
- /// Recurse up the tree.
- ///
- public const int Recurse = 1;
-
- ///
- /// Fallback to other languages.
- ///
- public const int Language = 2;
-
- ///
- /// Recurse up the tree. If content not found, fallback to other languages.
- ///
- public const int RecurseThenLanguage = 3;
-
- ///
- /// Fallback to other languages. If content not found, recurse up the tree.
- ///
- public const int LanguageThenRecurse = 4;
- }
- }
- }
-}
diff --git a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs
new file mode 100644
index 0000000000..0434218555
--- /dev/null
+++ b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.PublishedContent
+{
+ ///
+ /// Manages the built-in fallback policies.
+ ///
+ public struct Fallback : IEnumerable
+ {
+ private readonly int[] _values;
+
+ ///
+ /// Initializes a new instance of the struct with values.
+ ///
+ private Fallback(int[] values)
+ {
+ _values = values;
+ }
+
+ ///
+ /// Gets an ordered set of fallback policies.
+ ///
+ ///
+ public static Fallback To(params int[] values) => new Fallback(values);
+
+ ///
+ /// Do not fallback.
+ ///
+ public const int None = 0;
+
+ ///
+ /// Fallback to default value.
+ ///
+ public const int DefaultValue = 1;
+
+ ///
+ /// Gets the fallback to default value policy.
+ ///
+ public static Fallback ToDefaultValue => new Fallback(new[] { DefaultValue });
+
+ ///
+ /// Fallback to other languages.
+ ///
+ public const int Language = 2;
+
+ ///
+ /// Gets the fallback to language policy.
+ ///
+ public static Fallback ToLanguage => new Fallback(new[] { Language });
+
+ ///
+ /// Fallback to tree ancestors.
+ ///
+ public const int Ancestors = 3;
+
+ ///
+ /// Gets the fallback to tree ancestors policy.
+ ///
+ public static Fallback ToAncestors => new Fallback(new[] { Ancestors });
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return ((IEnumerable)_values ?? Array.Empty()).GetEnumerator();
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs
index c67df36c4f..b03b0515cf 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs
@@ -7,16 +7,6 @@
{
// fixme discussions & challenges
//
- // - what's with visitedLanguage? should be internal to fallback implementation
- // so that should be the case now, with latest changes
- //
- // - should be as simple as
- // model.Value("price", fallback: ValueFallback.Language);
- // model.Value("name", fallback: ValueFallback.Recurse);
- //
- // so chaining things through an array of ints is not... convenient
- // it feels like ppl could have ValueFallback.LanguageAndRecurse or something?
- //
// - the fallback: parameter value must be open, so about anything can be passed to the IPublishedValueFallback
// we have it now, it's an integer + constants, cool
//
@@ -24,14 +14,6 @@
// not! the default value of the fallback: parameter is 'default', not 'none', and if people
// want to implement a different default behavior, they have to override the fallback provider
//
- // - currently, no policies on IPublishedProperty nor IPublishedElement, but some may apply (language)
- // todo: implement
- //
- // - general defaultValue discussion:
- // when HasValue is false, the converter may return something, eg an empty enumerable, even though
- // defaultValue is null, so should we respect defaultValue only when it is not 'default'?
- // todo: when defaultValue==default, and HasValue is false, still return GetValue to ensure this
- //
// - (and...)
// ModelsBuilder model.Value(x => x.Price, ...) extensions need to be adjusted too
//
@@ -40,14 +22,15 @@
// OTOH we need to implement the readonly thing for languages
///
- /// Gets a fallback value for a property.
+ /// Tries to get a fallback value for a property.
///
/// The property.
/// The requested culture.
/// The requested segment.
+ /// A fallback strategy.
/// An optional default value.
- /// Integer value defining method to use for fallback when content not found
- /// A fallback value, or null.
+ /// The fallback value.
+ /// A value indicating whether a fallback value could be provided.
///
/// This method is called whenever property.Value(culture, segment, defaultValue) is called, and
/// property.HasValue(culture, segment) is false.
@@ -55,18 +38,19 @@
/// At property level, property.GetValue() does *not* implement fallback, and one has to
/// get property.Value() or property.Value{T}() to trigger fallback.
///
- object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback);
+ bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value);
///
- /// Gets a fallback value for a property.
+ /// Tries to get a fallback value for a property.
///
/// The type of the value.
/// The property.
/// The requested culture.
/// The requested segment.
+ /// A fallback strategy.
/// An optional default value.
- /// Integer value defining method to use for fallback when content not found
- /// A fallback value, or null.
+ /// The fallback value.
+ /// A value indicating whether a fallback value could be provided.
///
/// This method is called whenever property.Value{T}(culture, segment, defaultValue) is called, and
/// property.HasValue(culture, segment) is false.
@@ -74,76 +58,78 @@
/// At property level, property.GetValue() does *not* implement fallback, and one has to
/// get property.Value() or property.Value{T}() to trigger fallback.
///
- T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback);
+ bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value);
///
- /// Gets a fallback value for a published element property.
+ /// Tries to get a fallback value for a published element property.
///
/// The published element.
/// The property alias.
/// The requested culture.
/// The requested segment.
+ /// A fallback strategy.
/// An optional default value.
- /// Integer value defining method to use for fallback when content not found
- /// A fallback value, or null.
+ /// The fallback value.
+ /// A value indicating whether a fallback value could be provided.
///
/// This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.
/// It can only fallback at element level (no recurse).
///
- object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback);
+ bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value);
///
- /// Gets a fallback value for a published element property.
+ /// Tries to get a fallback value for a published element property.
///
/// The type of the value.
/// The published element.
/// The property alias.
/// The requested culture.
/// The requested segment.
+ /// A fallback strategy.
/// An optional default value.
- /// Integer value defining method to use for fallback when content not found
- /// A fallback value, or null.
+ /// The fallback value.
+ /// A value indicating whether a fallback value could be provided.
///
/// This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.
/// It can only fallback at element level (no recurse).
///
- T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback);
+ bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
///
- /// Gets a fallback value for a published content property.
+ /// Tries to get a fallback value for a published content property.
///
/// The published element.
/// The property alias.
/// The requested culture.
/// The requested segment.
+ /// A fallback strategy.
/// An optional default value.
- /// Integer value defining method to use for fallback when content not found
- /// A fallback value, or null.
+ /// The fallback value.
+ /// A value indicating whether a fallback value could be provided.
///
/// This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.
- /// fixme explain & document priority + merge w/recurse?
///
- object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback);
+ bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value);
///
- /// Gets a fallback value for a published content property.
+ /// Tries to get a fallback value for a published content property.
///
/// The type of the value.
/// The published element.
/// The property alias.
/// The requested culture.
/// The requested segment.
+ /// A fallback strategy.
/// An optional default value.
- /// Integer value defining method to use for fallback when content not found
- /// A fallback value, or null.
+ /// The fallback value.
+ /// A value indicating whether a fallback value could be provided.
///
/// This method is called whenever getting the property value for the specified alias, culture and
/// segment, either returned no property at all, or a property with HasValue(culture, segment) being false.
- /// fixme explain & document priority + merge w/recurse?
///
- T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback);
+ bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value);
}
}
diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs
index d920cefb24..cd7b063d44 100644
--- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedValueFallback.cs
@@ -9,21 +9,45 @@
public class NoopPublishedValueFallback : IPublishedValueFallback
{
///
- public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback) => defaultValue;
+ public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value)
+ {
+ value = default;
+ return false;
+ }
///
- public T GetValue(IPublishedProperty property, string culture, string segment, T defaultValue, int fallback) => defaultValue;
+ public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, T defaultValue, out T value)
+ {
+ value = default;
+ return false;
+ }
///
- public object GetValue(IPublishedElement content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue;
+ public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value)
+ {
+ value = default;
+ return false;
+ }
///
- public T GetValue(IPublishedElement content, string alias, string culture, string segment, T defaultValue, int fallback) => defaultValue;
+ public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
+ {
+ value = default;
+ return false;
+ }
///
- public object GetValue(IPublishedContent content, string alias, string culture, string segment, object defaultValue, int fallback) => defaultValue;
+ public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, object defaultValue, out object value)
+ {
+ value = default;
+ return false;
+ }
///
- public T GetValue(IPublishedContent content, string alias, string culture, string segment, T defaultValue, int fallback) => defaultValue;
+ public bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value)
+ {
+ value = default;
+ return false;
+ }
}
}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 41c9ea1df9..15c057389f 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -299,7 +299,6 @@
-
@@ -393,6 +392,7 @@
+
diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs
index c62cf9c0db..a063d2e387 100644
--- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs
+++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs
@@ -328,16 +328,16 @@ namespace Umbraco.Tests.Persistence.Repositories
// Add language to delete as a fall-back language to another one
var repository = CreateRepository(provider);
var languageToFallbackFrom = repository.Get(5);
- languageToFallbackFrom.FallbackLanguageId = 1;
+ languageToFallbackFrom.FallbackLanguageId = 2; // fall back to #2 (something we can delete)
repository.Save(languageToFallbackFrom);
- // Act
- var languageToDelete = repository.Get(1);
+ // delete #2
+ var languageToDelete = repository.Get(2);
repository.Delete(languageToDelete);
- var exists = repository.Exists(1);
+ var exists = repository.Exists(2);
- // Assert
+ // has been deleted
Assert.That(exists, Is.False);
}
}
@@ -369,6 +369,8 @@ namespace Umbraco.Tests.Persistence.Repositories
private void CreateTestData()
{
+ //Id 1 is en-US - when Umbraco is installed
+
var languageDK = new Language("da-DK") { CultureName = "da-DK" };
ServiceContext.LocalizationService.Save(languageDK);//Id 2
diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs
index d5e01fd424..4e98aea000 100644
--- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs
+++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs
@@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reflection;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
+using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Tests.Testing;
using Umbraco.Web;
@@ -63,6 +65,8 @@ namespace Umbraco.Tests.PublishedContent
var props = new[]
{
factory.CreatePropertyType("prop1", 1),
+ factory.CreatePropertyType("welcomeText", 1),
+ factory.CreatePropertyType("welcomeText2", 1),
};
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), props);
@@ -168,7 +172,7 @@ namespace Umbraco.Tests.PublishedContent
public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
- var value = content.Value("welcomeText", "es", fallback: Core.Constants.Content.ValueFallback.Language);
+ var value = content.Value("welcomeText", "es", fallback: Fallback.ToLanguage);
Assert.AreEqual("Welcome", value);
}
@@ -176,7 +180,7 @@ namespace Umbraco.Tests.PublishedContent
public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_Over_Two_Levels()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
- var value = content.Value("welcomeText", "it", fallback: Core.Constants.Content.ValueFallback.Language);
+ var value = content.Value("welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors));
Assert.AreEqual("Welcome", value);
}
@@ -184,7 +188,7 @@ namespace Umbraco.Tests.PublishedContent
public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First();
- var value = content.Value("welcomeText", "no", fallback: Core.Constants.Content.ValueFallback.Language);
+ var value = content.Value("welcomeText", "no", fallback: Fallback.ToLanguage);
Assert.IsNull(value);
}
@@ -200,7 +204,7 @@ namespace Umbraco.Tests.PublishedContent
public void Can_Get_Content_Recursively()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
- var value = content.Value("welcomeText2", fallback: Core.Constants.Content.ValueFallback.Recurse);
+ var value = content.Value("welcomeText2", fallback: Fallback.ToAncestors);
Assert.AreEqual("Welcome", value);
}
@@ -208,7 +212,7 @@ namespace Umbraco.Tests.PublishedContent
public void Can_Get_Content_With_Recursive_Priority()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
- var value = content.Value("welcomeText", "nl", fallback: Core.Constants.Content.ValueFallback.RecurseThenLanguage);
+ var value = content.Value("welcomeText", "nl", fallback: Fallback.To(Fallback.Ancestors, Fallback.Language));
// No Dutch value is directly assigned. Check has fallen back to Dutch value from parent.
Assert.AreEqual("Welkom", value);
@@ -218,7 +222,7 @@ namespace Umbraco.Tests.PublishedContent
public void Can_Get_Content_With_Fallback_Language_Priority()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
- var value = content.Value("welcomeText", "nl", fallback: Core.Constants.Content.ValueFallback.LanguageThenRecurse);
+ var value = content.Value("welcomeText", "nl", fallback: Fallback.ToLanguage);
// No Dutch value is directly assigned. Check has fallen back to English value from language variant.
Assert.AreEqual("Welcome", value);
@@ -228,7 +232,48 @@ namespace Umbraco.Tests.PublishedContent
public void Throws_For_Non_Supported_Fallback()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
- Assert.Throws(() => content.Value("welcomeText", "nl", fallback: 999));
+ Assert.Throws(() => content.Value("welcomeText", "nl", fallback: Fallback.To(999)));
+ }
+
+ [Test]
+ public void Can_Fallback_To_Default_Value()
+ {
+ var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
+
+ // no Dutch value is assigned, so getting null
+ var value = content.Value("welcomeText", "nl");
+ Assert.IsNull(value);
+
+ // even if we 'just' provide a default value
+ value = content.Value("welcomeText", "nl", defaultValue: "woop");
+ Assert.IsNull(value);
+
+ // but it works with proper fallback settings
+ value = content.Value("welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop");
+ Assert.AreEqual("woop", value);
+ }
+
+ [Test]
+ public void Can_Have_Custom_Default_Value()
+ {
+ var content = UmbracoContext.Current.ContentCache.GetAtRoot().First().Children.First();
+
+ // hack the value, pretend the converter would return something
+ var prop = content.GetProperty("welcomeText") as SolidPublishedPropertyWithLanguageVariants;
+ Assert.IsNotNull(prop);
+ prop.SetValue("nl", "nope"); // HasValue false but getting value returns this
+
+ // there is an EN value
+ var value = content.Value("welcomeText", "en-US");
+ Assert.AreEqual("Welcome", value);
+
+ // there is no NL value and we get the 'converted' value
+ value = content.Value("welcomeText", "nl");
+ Assert.AreEqual("nope", value);
+
+ // but it works with proper fallback settings
+ value = content.Value("welcomeText", "nl", fallback: Fallback.ToDefaultValue, defaultValue: "woop");
+ Assert.AreEqual("woop", value);
}
}
}
diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs
index e8ea3bc829..14dae46bcb 100644
--- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs
+++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs
@@ -341,8 +341,8 @@ namespace Umbraco.Tests.PublishedContent
public void Get_Property_Value_Recursive()
{
var doc = GetNode(1174);
- var rVal = doc.Value("testRecursive", fallback: Constants.Content.ValueFallback.Recurse);
- var nullVal = doc.Value("DoNotFindThis", fallback: Constants.Content.ValueFallback.Recurse);
+ var rVal = doc.Value("testRecursive", fallback: Fallback.ToAncestors);
+ var nullVal = doc.Value("DoNotFindThis", fallback: Fallback.ToAncestors);
Assert.AreEqual("This is the recursive val", rVal);
Assert.AreEqual(null, nullVal);
}
diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs
index bfcd339650..4e9cdaa8f9 100644
--- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs
+++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
-using ValueFallback = Umbraco.Core.Constants.Content.ValueFallback;
namespace Umbraco.Web.Models.PublishedContent
{
@@ -24,82 +23,110 @@ namespace Umbraco.Web.Models.PublishedContent
}
///
- public object GetValue(IPublishedProperty property, string culture, string segment, object defaultValue, int fallback)
+ public bool TryGetValue(IPublishedProperty property, string culture, string segment, Fallback fallback, object defaultValue, out object value)
{
- return GetValue