diff --git a/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs b/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs index 4c6811d9..4fe46a19 100644 --- a/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs +++ b/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs @@ -93,7 +93,7 @@ public static TResult GetSetting(this HandlerSettings settings, string { if (settings.Settings != null && settings.Settings.TryGetValue(key, out var value) && value is not null) { - if (value.TryConvertPreChecked(out var result) && result is not null) + if (value.TryGetValueAs(out var result) && result is not null) return result; } diff --git a/uSync.Core/Extensions/ConversionExtensions.cs b/uSync.Core/Extensions/ConversionExtensions.cs index d04d2bd7..3ca1616d 100644 --- a/uSync.Core/Extensions/ConversionExtensions.cs +++ b/uSync.Core/Extensions/ConversionExtensions.cs @@ -4,7 +4,7 @@ internal static class ConversionExtensions public static TObject? GetValueAs(this object value) { if (value == null) return default; - return value.TryConvertPreChecked(out var result) ? result : default; + return value.TryGetValueAs(out var result) ? result : default; } public static Guid ConvertToGuid(this int value) diff --git a/uSync.Core/Extensions/JsonTextExtensions.cs b/uSync.Core/Extensions/JsonTextExtensions.cs index 86d8424e..ee4edfd8 100644 --- a/uSync.Core/Extensions/JsonTextExtensions.cs +++ b/uSync.Core/Extensions/JsonTextExtensions.cs @@ -390,38 +390,63 @@ public static bool TrySerializeJsonString(this object value, [MaybeNull] out str public static string SerializeJsonString(this object value, bool indent = true) => value is null ? string.Empty : JsonSerializer.Serialize(value, indent ? _defaultOptions : _flatOptions); - private static bool TryGetValueAs(this object value, [MaybeNullWhen(false)] out TObject result) + /// + /// Convert a value to the requested type. + /// + /// + /// Pre-empts the first-chance InvalidCastException that Umbraco's TryConvertTo + /// throws when converting a JsonElement to a value type (see uSync.Complete + /// issue #304). Settings/config values often arrive as JsonElement (bound from + /// appsettings.json); doing that conversion with System.Text.Json first means the + /// common path never throws. String conversions (which TryConvertTo already + /// handles cleanly) and anything STJ can't handle still fall back to TryConvertTo. + /// + public static bool TryGetValueAs(this object? value, [MaybeNullWhen(false)] out TObject result) { result = default; - if (value == null) return false; + if (value is null) return false; + + // Umbraco's TryConvertTo turns a JsonElement into a string cleanly, but throws + // (and swallows) an InvalidCastException for JsonElement -> value type. Do the + // value-type conversion with System.Text.Json first to avoid that noise; string + // and anything STJ can't handle fall through to TryConvertTo below. + if (value is JsonElement element && typeof(TObject) != typeof(string)) + { + try + { + result = element.Deserialize(_defaultOptions); + if (result is not null) return true; + } + catch + { + // not something STJ could convert directly - fall back to TryConvertTo below. + } + } + var attempt = value.TryConvertTo(); - if (attempt is false || attempt.Result is null) return attempt; + if (attempt.Success is false || attempt.Result is null) return false; + result = attempt.Result; return true; } /// - /// Convert a value to the requested type, pre-empting the first-chance - /// InvalidCastException that Umbraco's TryConvertTo throws when converting - /// a JsonElement to a value type (see uSync.Complete issue #304). + /// Convert a value to the requested runtime type. /// /// - /// Settings/config values often arrive as JsonElement (bound from appsettings.json). - /// Asking Umbraco's TryConvertTo to turn one into e.g. a bool throws (and swallows) - /// an InvalidCastException every call - harmless, but noisy and slow when a debugger - /// is attached. Doing the JsonElement conversion with System.Text.Json first means the - /// common path never throws; anything STJ can't handle still falls back to TryConvertTo. + /// Non-generic companion to the generic TryGetValueAs for callers that only + /// have a runtime Type. Same JsonElement pre-check. /// - public static bool TryConvertPreChecked(this object? value, [MaybeNullWhen(false)] out TObject result) + public static bool TryGetValueAs(this object? value, Type targetType, [MaybeNullWhen(false)] out object result) { result = default; if (value is null) return false; - if (value is JsonElement element) + if (value is JsonElement element && targetType != typeof(string)) { try { - result = element.Deserialize(_defaultOptions); + result = element.Deserialize(targetType, _defaultOptions); if (result is not null) return true; } catch @@ -430,7 +455,7 @@ public static bool TryConvertPreChecked(this object? value, [MaybeNullW } } - var attempt = value.TryConvertTo(); + var attempt = value.TryConvertTo(targetType); if (attempt.Success is false || attempt.Result is null) return false; result = attempt.Result; @@ -498,8 +523,7 @@ public static TResult GetPropertyValueOrDefault(this JsonObject obj, st if (obj.TryGetPropertyValue(propertyName, out var value) is false || value is null) return defaultValue; - var attempt = value.TryConvertTo(); - return attempt.ResultOr(defaultValue); + return value.TryGetValueAs(out var result) ? result : defaultValue; } public static bool TryGetPropertyAsArray(this JsonObject jsonObject, string propertyName, [MaybeNullWhen(false)] out JsonArray result) diff --git a/uSync.Core/Extensions/ListExtensions.cs b/uSync.Core/Extensions/ListExtensions.cs index e4005fdf..d22b0149 100644 --- a/uSync.Core/Extensions/ListExtensions.cs +++ b/uSync.Core/Extensions/ListExtensions.cs @@ -1,5 +1,7 @@ using Umbraco.Extensions; +using uSync.Core.Extensions; + namespace uSync.Core; public static class ListExtensions @@ -40,10 +42,9 @@ internal static IEnumerable ConvertItems(this IList items) foreach (var item in items) { if (string.IsNullOrWhiteSpace(item)) continue; - var attempt = item.TryConvertTo(); - if (attempt.Success && attempt.Result is not null) + if (item.TryGetValueAs(out var result)) { - yield return attempt.Result; + yield return result; } } } diff --git a/uSync.Core/Extensions/ObjectPropertyExtensions.cs b/uSync.Core/Extensions/ObjectPropertyExtensions.cs index e7bcc9a2..cd28752e 100644 --- a/uSync.Core/Extensions/ObjectPropertyExtensions.cs +++ b/uSync.Core/Extensions/ObjectPropertyExtensions.cs @@ -75,11 +75,6 @@ private static TValue GetPropertyAs(PropertyInfo info, object property, var value = info.GetValue(property); if (value == null) return defaultValue; - var result = value.TryConvertTo(); - if (result.Success) - return result.Result ?? defaultValue; - - return defaultValue; - + return value.TryGetValueAs(out var result) ? result : defaultValue; } } diff --git a/uSync.Core/Extensions/XElementExtensions.cs b/uSync.Core/Extensions/XElementExtensions.cs index 7f91c281..badef56e 100644 --- a/uSync.Core/Extensions/XElementExtensions.cs +++ b/uSync.Core/Extensions/XElementExtensions.cs @@ -5,6 +5,8 @@ using Umbraco.Extensions; +using uSync.Core.Extensions; + namespace uSync.Core; public static class XElementExtensions @@ -138,11 +140,7 @@ public static TObject ValueOrDefault([AllowNull] this XElement? node, T var value = node.ValueOrDefault(string.Empty); if (value == string.Empty) return defaultValue; - var attempt = value.TryConvertTo(); - if (attempt) - return attempt.Result ?? defaultValue; - - return defaultValue; + return value.TryGetValueAs(out var result) ? result : defaultValue; } @@ -209,8 +207,7 @@ public static void CreateOrSetElement(this XElement node, string name, { if (node is null) return; - var attempt = value.TryConvertTo(); - if (attempt.Success) + if (value.TryGetValueAs(out var stringValue)) { var element = node.Element(name); if (element is null) @@ -219,7 +216,7 @@ public static void CreateOrSetElement(this XElement node, string name, node.Add(element); } - element.Value = attempt.Result ?? string.Empty; + element.Value = stringValue ?? string.Empty; } } @@ -289,11 +286,7 @@ public static TObject ValueOrDefault([AllowNull] this XAttribute attrib var value = attribute.ValueOrDefault(string.Empty); if (value == string.Empty) return defaultValue; - var attempt = value.TryConvertTo(); - if (attempt) - return attempt.Result ?? defaultValue; - - return defaultValue; + return value.TryGetValueAs(out var result) ? result : defaultValue; } #endregion diff --git a/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs b/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs index 0c08a0ec..97103d03 100644 --- a/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs +++ b/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs @@ -88,9 +88,8 @@ private static Guid GetGuidValue(JsonObject obj, string key) { if (obj != null && obj.ContainsKey(key)) { - var attempt = obj[key]?.ToString().TryConvertTo(); - if (attempt?.Success is true) - return attempt?.Result ?? Guid.Empty; + if (obj[key]?.ToString().TryGetValueAs(out var guid) is true) + return guid; } return Guid.Empty; diff --git a/uSync.Core/Mapping/Mappers/MemberGroupPickerManager.cs b/uSync.Core/Mapping/Mappers/MemberGroupPickerManager.cs index 4ebafba2..14eee395 100644 --- a/uSync.Core/Mapping/Mappers/MemberGroupPickerManager.cs +++ b/uSync.Core/Mapping/Mappers/MemberGroupPickerManager.cs @@ -3,6 +3,7 @@ using Umbraco.Extensions; using uSync.Core.Dependency; +using uSync.Core.Extensions; using uSync.Core.Serialization; using static Umbraco.Cms.Core.Constants; @@ -30,11 +31,10 @@ public MemberGroupPickerMapper( /// public override async Task GetExportValueAsync(object value, string editorAlias) { - var attempt = value.TryConvertTo(); - if (attempt.Success is false || attempt.Result is null) + if (value.TryGetValueAs(out var stringValue) is false) return await base.GetExportValueAsync(value, editorAlias); - var values = attempt.Result.ToDelimitedList().ConvertItems(); + var values = stringValue.ToDelimitedList().ConvertItems(); var groups = new List(); @@ -86,11 +86,10 @@ public override async Task> GetDependenciesAsync(ob return Enumerable.Empty(); // get the int value and load the group - var attempt = value.TryConvertTo(); - if (attempt.Success is false || attempt.Result is null) + if (value.TryGetValueAs(out var stringValue) is false) return await base.GetDependenciesAsync(value, editorAlias, flags); - var values = attempt.Result.ToDelimitedList().ConvertItems(); + var values = stringValue.ToDelimitedList().ConvertItems(); var dependencies = new List(); diff --git a/uSync.Core/Mapping/SyncValueMapperBase.cs b/uSync.Core/Mapping/SyncValueMapperBase.cs index 5d07c36c..36cfe02a 100644 --- a/uSync.Core/Mapping/SyncValueMapperBase.cs +++ b/uSync.Core/Mapping/SyncValueMapperBase.cs @@ -115,7 +115,7 @@ protected IEnumerable CreateDependencies(IEnumerable ud protected static TObject? GetValueAs(object value) { if (value == null) return default; - return value.TryConvertPreChecked(out var result) ? result : default; + return value.TryGetValueAs(out var result) ? result : default; } } diff --git a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs index 328efedc..c24fe3b6 100644 --- a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs +++ b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs @@ -706,8 +706,8 @@ private static bool IsUpdatedValue(object? current, object? newValue) if (current != null && newValue != null && current.GetType() != newValue.GetType()) { var currentType = current.GetType(); - var attempt = newValue.TryConvertTo(currentType); - if (attempt.Success) return !current.Equals(attempt.Result); + if (newValue.TryGetValueAs(currentType, out var converted)) + return !current.Equals(converted); } return true; diff --git a/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs b/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs index 80c4776a..2f4edd84 100644 --- a/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs @@ -153,17 +153,15 @@ protected void SerializeNewProperty(XElement node, IPropertyType propert { var value = propertyInfo.GetValue(property); - var attempt = value.TryConvertTo(); - if (attempt.Success) + // TryGetValueAs treats a null conversion result as failure, so fall back + // to an empty element - the property still gets recorded in the xml. + if (value.TryGetValueAs(out var converted)) { - if (attempt.Result != null) - { - node.Add(new XElement(propertyName, attempt.Result)); - } - else - { - node.Add(new XElement(propertyName, string.Empty)); - } + node.Add(new XElement(propertyName, converted)); + } + else + { + node.Add(new XElement(propertyName, string.Empty)); } } } @@ -727,19 +725,18 @@ protected void AddAlias(string alias) if (propertyInfo != null) { var value = node.Element(propertyName).ValueOrDefault(string.Empty); - var attempt = value.TryConvertTo(); - if (attempt.Success) + if (value.TryGetValueAs(out var converted)) { var current = ContentTypeBaseSerializer.GetPropertyAs(propertyInfo, property); - if (current == null || !current.Equals(attempt.Result)) + if (current == null || !current.Equals(converted)) { - propertyInfo.SetValue(property, attempt.Result); + propertyInfo.SetValue(property, converted); return uSyncChange.Update($"property/{propertyName}", propertyName, current.ToNonBlankValue(), - attempt.Result?.ToString()); + converted?.ToString()); } } } @@ -754,12 +751,7 @@ protected void AddAlias(string alias) var value = info.GetValue(property); if (value == null) return default; - var result = value.TryConvertTo(); - if (result.Success) - return result.Result; - - return default; - + return value.TryGetValueAs(out var result) ? result : default; } diff --git a/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs b/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs index e04e21d1..02a95b14 100644 --- a/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs @@ -409,15 +409,14 @@ private List DeserializeCleanupHistory(IContentType item, XElement var current = GetPropertyAs(property, historyCleanup); if (element.Value != current) { - // now set it. - var updatedValue = element.Value.TryConvertTo(property.PropertyType); - if (updatedValue.Success) + // now set it. + if (element.Value.TryGetValueAs(property.PropertyType, out var updatedValue)) { if (logger.IsEnabled(LogLevel.Debug)) - logger.LogDebug("Saving HistoryCleanup Value: {name} {value}", element.Name.LocalName, updatedValue.Result); + logger.LogDebug("Saving HistoryCleanup Value: {name} {value}", element.Name.LocalName, updatedValue); - changes.AddUpdate($"{_historyCleanupName}:{element.Name.LocalName}", current.ToNonBlankValue(), updatedValue.Result, $"{_historyCleanupName}/{element.Name.LocalName}"); - property.SetValue(historyCleanup, updatedValue.Result); + changes.AddUpdate($"{_historyCleanupName}:{element.Name.LocalName}", current.ToNonBlankValue(), updatedValue, $"{_historyCleanupName}/{element.Name.LocalName}"); + property.SetValue(historyCleanup, updatedValue); } } } @@ -451,11 +450,6 @@ protected override XElement CleanseNode(XElement node) var value = info.GetValue(property); if (value is null) return default; - var result = value.TryConvertTo(); - if (result.Success) - return result.Result; - - return default; - + return value.TryGetValueAs(out var result) ? result : default; } } diff --git a/uSync.Core/Serialization/Serializers/DomainSerializer.cs b/uSync.Core/Serialization/Serializers/DomainSerializer.cs index a9860954..5977c05d 100644 --- a/uSync.Core/Serialization/Serializers/DomainSerializer.cs +++ b/uSync.Core/Serialization/Serializers/DomainSerializer.cs @@ -167,8 +167,7 @@ private static int GetSortableValue(IDomain item) var result = property.GetValue(item); - var attempt = result.TryConvertTo(); - return attempt.Success ? attempt.Result : 0; + return result.TryGetValueAs(out var sortable) ? sortable : 0; } /// diff --git a/uSync.Core/Serialization/SyncSerializerOptions.cs b/uSync.Core/Serialization/SyncSerializerOptions.cs index 6426ac88..65ffa680 100644 --- a/uSync.Core/Serialization/SyncSerializerOptions.cs +++ b/uSync.Core/Serialization/SyncSerializerOptions.cs @@ -74,7 +74,7 @@ public TResult GetSetting(string key, TResult defaultValue) { if (this.Settings?.TryGetValue(key, out var value) is true && value is not null) { - if (value.TryConvertPreChecked(out var result) && result is not null) + if (value.TryGetValueAs(out var result) && result is not null) return result; } diff --git a/uSync.Tests/Extensions/TryConvertPreCheckedTests.cs b/uSync.Tests/Extensions/TryGetValueAsTests.cs similarity index 81% rename from uSync.Tests/Extensions/TryConvertPreCheckedTests.cs rename to uSync.Tests/Extensions/TryGetValueAsTests.cs index 048c31dd..9005a57d 100644 --- a/uSync.Tests/Extensions/TryConvertPreCheckedTests.cs +++ b/uSync.Tests/Extensions/TryGetValueAsTests.cs @@ -13,14 +13,14 @@ namespace uSync.Tests.Extensions; /// (uSync.Complete issue #304). /// [TestFixture] -internal class TryConvertPreCheckedTests +internal class TryGetValueAsTests { [Test] public void JsonElementTrue_ConvertsToBool() { object value = JsonSerializer.SerializeToElement(true); - var success = value.TryConvertPreChecked(out var result); + var success = value.TryGetValueAs(out var result); Assert.Multiple(() => { @@ -34,7 +34,7 @@ public void JsonElementFalse_ConvertsToBool() { object value = JsonSerializer.SerializeToElement(false); - var success = value.TryConvertPreChecked(out var result); + var success = value.TryGetValueAs(out var result); Assert.Multiple(() => { @@ -48,7 +48,7 @@ public void JsonElementNumber_ConvertsToInt() { object value = JsonSerializer.SerializeToElement(42); - var success = value.TryConvertPreChecked(out var result); + var success = value.TryGetValueAs(out var result); Assert.Multiple(() => { @@ -63,7 +63,7 @@ public void JsonElementString_ConvertsToGuid() var guid = Guid.NewGuid(); object value = JsonSerializer.SerializeToElement(guid.ToString()); - var success = value.TryConvertPreChecked(out var result); + var success = value.TryGetValueAs(out var result); Assert.Multiple(() => { @@ -77,7 +77,7 @@ public void JsonElementString_ConvertsToString() { object value = JsonSerializer.SerializeToElement("hello"); - var success = value.TryConvertPreChecked(out var result); + var success = value.TryGetValueAs(out var result); Assert.Multiple(() => { @@ -93,7 +93,7 @@ public void PlainString_ConvertsToInt_ViaFallback() { object value = "42"; - var success = value.TryConvertPreChecked(out var result); + var success = value.TryGetValueAs(out var result); Assert.Multiple(() => { @@ -107,7 +107,7 @@ public void Null_ReturnsFalse() { object? value = null; - var success = value.TryConvertPreChecked(out var result); + var success = value.TryGetValueAs(out var result); Assert.Multiple(() => {