diff --git a/src/Speckle.Sdk.Dependencies/SpeckleHttpClientHandler.cs b/src/Speckle.Sdk.Dependencies/SpeckleHttpClientHandler.cs index 7df830c5..88db835e 100644 --- a/src/Speckle.Sdk.Dependencies/SpeckleHttpClientHandler.cs +++ b/src/Speckle.Sdk.Dependencies/SpeckleHttpClientHandler.cs @@ -41,13 +41,7 @@ internal sealed class SpeckleHttpClientHandler : DelegatingHandler activity?.InjectHeaders((k, v) => request.Headers.TryAddWithoutValidation(k, v)); var policyResult = await _resiliencePolicy - .ExecuteAndCaptureAsync( - ctx => - { - return base.SendAsync(request, cancellationToken); - }, - context - ) + .ExecuteAndCaptureAsync(ctx => base.SendAsync(request, cancellationToken), context) .ConfigureAwait(false); context.TryGetValue("retryCount", out var retryCount); activity?.SetTag("retryCount", retryCount); diff --git a/src/Speckle.Sdk/Api/GraphQL/Client.cs b/src/Speckle.Sdk/Api/GraphQL/Client.cs index c805287e..426415ad 100644 --- a/src/Speckle.Sdk/Api/GraphQL/Client.cs +++ b/src/Speckle.Sdk/Api/GraphQL/Client.cs @@ -127,10 +127,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient activity?.SetStatus(SdkActivityStatusCode.Ok); return ret; } - catch (Exception ex) + catch (Exception) { activity?.SetStatus(SdkActivityStatusCode.Error); - activity?.RecordException(ex); + // Don't record exception as it's rethrown. throw; } } diff --git a/src/Speckle.Sdk/Host/TypeLoader.cs b/src/Speckle.Sdk/Host/TypeLoader.cs index bae0d151..ad9a452b 100644 --- a/src/Speckle.Sdk/Host/TypeLoader.cs +++ b/src/Speckle.Sdk/Host/TypeLoader.cs @@ -15,10 +15,14 @@ internal static class TypeLoader private static ConcurrentDictionary s_cachedTypes = new(); private static ConcurrentDictionary s_fullTypeStrings = new(); private static ConcurrentDictionary s_jsonPropertyAttribute = new(); + private static readonly ConcurrentDictionary s_obsolete = new(); private static ConcurrentDictionary> s_propInfoCache = new(); public static IEnumerable Types => s_availableTypes; + public static bool IsObsolete(PropertyInfo property) => + s_obsolete.GetOrAdd(property, p => p.IsDefined(typeof(ObsoleteAttribute), true)); + public static JsonPropertyAttribute? GetJsonPropertyAttribute(PropertyInfo property) => s_jsonPropertyAttribute.GetOrAdd(property, p => p.GetCustomAttribute(true)); diff --git a/src/Speckle.Sdk/Models/Base.cs b/src/Speckle.Sdk/Models/Base.cs index 33a4886a..57fb1408 100644 --- a/src/Speckle.Sdk/Models/Base.cs +++ b/src/Speckle.Sdk/Models/Base.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Speckle.Newtonsoft.Json; using Speckle.Newtonsoft.Json.Linq; -using Speckle.Sdk.Common; using Speckle.Sdk.Helpers; using Speckle.Sdk.Host; using Speckle.Sdk.Serialisation; @@ -92,8 +91,7 @@ public class Base : DynamicBase, ISpeckleObject var typedProps = @base.GetInstanceMembers(); foreach (var prop in typedProps.Where(p => p.CanRead)) { - bool isIgnored = - prop.IsDefined(typeof(ObsoleteAttribute), true) || prop.IsDefined(typeof(JsonIgnoreAttribute), true); + bool isIgnored = TypeLoader.IsObsolete(prop) || prop.IsDefined(typeof(JsonIgnoreAttribute), true); if (isIgnored) { continue; @@ -193,30 +191,4 @@ public class Base : DynamicBase, ISpeckleObject return count; } } - - /// - /// Creates a shallow copy of the current base object. - /// This operation does NOT copy/duplicate the data inside each prop. - /// The new object's property values will be pointers to the original object's property value. - /// - /// A shallow copy of the original object. - public Base ShallowCopy() - { - Type type = GetType(); - Base myDuplicate = (Base)Activator.CreateInstance(type).NotNull(); - myDuplicate.id = id; - myDuplicate.applicationId = applicationId; - - foreach (var kvp in GetMembers()) - { - var propertyInfo = type.GetProperty(kvp.Key); - if (propertyInfo is not null && !propertyInfo.CanWrite) - { - continue; - } - myDuplicate[kvp.Key] = kvp.Value; - } - - return myDuplicate; - } } diff --git a/src/Speckle.Sdk/Models/DynamicBase.cs b/src/Speckle.Sdk/Models/DynamicBase.cs index e7f3e953..f51a3dfa 100644 --- a/src/Speckle.Sdk/Models/DynamicBase.cs +++ b/src/Speckle.Sdk/Models/DynamicBase.cs @@ -7,7 +7,7 @@ namespace Speckle.Sdk.Models; /// /// Base class implementing a bunch of nice dynamic object methods, like adding and removing props dynamically. Makes c# feel like json. -/// Orginally adapted from Rick Strahl 🤘 +/// Originally adapted from Rick Strahl 🤘 /// https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object /// public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider @@ -84,6 +84,44 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider } } + /// + /// Creates a shallow copy of the current base object. + /// This operation does NOT copy/duplicate the data inside each prop. + /// The new object's property values will be pointers to the original object's property value. + /// + /// A shallow copy of the original object. + public DynamicBase ShallowCopy() + { + Type type = GetType(); + DynamicBase myDuplicate = (DynamicBase)( + Activator.CreateInstance(type) ?? throw new SpeckleException($"Failed to create instance of {type.Name}") + ); + + // Add dynamic members + foreach (var kvp in _properties) + { + myDuplicate._properties[kvp.Key] = kvp.Value; + } + + var pinfos = TypeLoader.GetBaseProperties(type).Where(x => !TypeLoader.IsObsolete(x)); + foreach (var pi in pinfos) + { + if (pi.CanWrite) + { + try + { + pi.SetValue(myDuplicate, pi.GetValue(this)); + } + catch (Exception ex) when (!ex.IsFatal()) + { + throw new SpeckleException($"Failed to set value for {type.Name}.{pi.Name}", ex); + } + } + } + + return myDuplicate; + } + /// /// /// Gets properties via the dot syntax. @@ -232,7 +270,7 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider .GetBaseProperties(GetType()) .Where(x => { - var hasObsolete = x.IsDefined(typeof(ObsoleteAttribute), true); + var hasObsolete = TypeLoader.IsObsolete(x); // If obsolete is false and prop has obsolete attr // OR diff --git a/tests/Speckle.Sdk.Tests.Unit/Helpers/Path.cs b/tests/Speckle.Sdk.Tests.Unit/Helpers/Path.cs index 73280f59..69d7d95e 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Helpers/Path.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Helpers/Path.cs @@ -18,7 +18,7 @@ public class SpecklePathTests } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - pattern = @"\/Users\/.*\/\.config"; + pattern = @"\/Users\/.*\/Library\/Application Support"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { @@ -57,7 +57,7 @@ public class SpecklePathTests } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - pattern = @"\/Users\/.*\/\.config"; + pattern = @"\/Users\/.*\/Library\/Application Support"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { diff --git a/tests/Speckle.Sdk.Tests.Unit/Models/BaseTests.cs b/tests/Speckle.Sdk.Tests.Unit/Models/BaseTests.cs index dd7da7dc..4b37aebf 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Models/BaseTests.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Models/BaseTests.cs @@ -213,7 +213,9 @@ public class BaseTests [Fact] public void CanShallowCopy() { - var sample = new SampleObject(); + var sample = new SampleObject { id = "sampleId" }; + dynamic x = sample; + x.test = "test"; var copy = sample.ShallowCopy(); var selectedMembers = DynamicBaseMemberType.Dynamic | DynamicBaseMemberType.Instance; diff --git a/tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs b/tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs new file mode 100644 index 00000000..4654faae --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs @@ -0,0 +1,141 @@ +using FluentAssertions; +using Microsoft.CSharp.RuntimeBinder; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; + +namespace Speckle.Sdk.Tests.Unit.Models; + +public class DynamicBaseTests +{ + public DynamicBaseTests() + { + TypeLoader.Reset(); + TypeLoader.Initialize(typeof(Base).Assembly, typeof(BaseTests).Assembly); + } + + [Fact] + public void Indexer_SetAndGet() + { + // Arrange + var dynamicBase = new DynamicBase(); + var key = "testProperty"; + var value = "testValue"; + + // Act + dynamicBase[key] = value; + var result = dynamicBase[key]; + + // Assert + result.Should().Be(value); + } + + [Fact] + public void DynamicProperty_SetAndGet() + { + // Arrange + dynamic dynamicBase = new DynamicBase(); + var value = "dynamicValue"; + + // Act + dynamicBase.dynamicProperty = value; + object result = dynamicBase.dynamicProperty; + + // Assert + result.Should().Be(value); + } + + [Fact] + public void GetMembers_Default() + { + // Arrange + dynamic dynamicBase = new DynamicBase(); + dynamicBase.dynamicProp = "hello"; + + // Act + IDictionary members = dynamicBase.GetMembers(); + + // Assert + members.Should().ContainKey("dynamicProp"); + } + + [Fact] + public void GetMembers_Instance() + { + // Arrange + var dynamicBase = new TestDynamicBase(); + + // Act + var members = dynamicBase.GetMembers(DynamicBaseMemberType.Instance); + + // Assert + members.Should().ContainKey(nameof(TestDynamicBase.InstanceProperty)); + members.Should().NotContainKey("dynamicProp"); + } + + [Fact] + public void GetDynamicMemberNames() + { + // Arrange + dynamic dynamicBase = new DynamicBase(); + dynamicBase.prop1 = 1; + dynamicBase.prop2 = "test"; + + // Act + IEnumerable memberNames = dynamicBase.GetDynamicMemberNames(); + + // Assert + memberNames.Should().BeEquivalentTo(["DynamicPropertyKeys", "prop1", "prop2"]); + } + + [Fact] + public void TryGetMember_Existing() + { + // Arrange + dynamic dynamicBase = new DynamicBase(); + dynamicBase.existingProp = "I exist"; + + // Act + var result = dynamicBase.existingProp; + + // Assert + ((object)result) + .Should() + .Be("I exist"); + } + + [Fact] + public void TryGetMember_NonExisting() + { + // Arrange + dynamic dynamicBase = new DynamicBase(); + + // Act + Action act = () => + { + var result = dynamicBase.nonExistingProp; + }; + + // Assert + act.Should().Throw(); + } + + [Fact] + public void TrySetMember() + { + // Arrange + dynamic dynamicBase = new DynamicBase(); + + // Act + dynamicBase.newProp = "newValue"; + + // Assert + ((object)dynamicBase.newProp) + .Should() + .Be("newValue"); + } + + private class TestDynamicBase : DynamicBase + { + public string InstanceProperty { get; set; } = "instance"; + } +}