diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs
index 8c4b6c0..d66879c 100644
--- a/src/DynamicObj/DynamicObj.fs
+++ b/src/DynamicObj/DynamicObj.fs
@@ -245,6 +245,36 @@ type DynamicObj() =
)
///
+ ///
+ ///
+ /// The target object to copy dynamic members to
+ /// Whether existing properties on the target object will be overwritten
+ member this.DeepCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
+ let overWrite = defaultArg overWrite false
+ let rec tryDeepCopyObj (o:obj) =
+ match o with
+ | :? DynamicObj as dyn ->
+ let newDyn = DynamicObj()
+ for kv in (dyn.GetProperties(false)) do
+ newDyn.SetProperty(kv.Key, tryDeepCopyObj kv.Value)
+ box newDyn
+ | :? array as dyns ->
+ box [|for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj|]
+ | :? list as dyns ->
+ box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]
+ | :? ResizeArray as dyns ->
+ box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]))
+ //| :? System.ICloneable as clonable -> clonable.Clone()
+ | _ -> o
+
+ this.GetProperties(false)
+ |> Seq.iter (fun kv ->
+ match target.TryGetPropertyHelper kv.Key with
+ | Some pi when overWrite -> pi.SetValue target (tryDeepCopyObj kv.Value)
+ | Some _ -> ()
+ | None -> target.SetProperty(kv.Key, tryDeepCopyObj kv.Value)
+ )
+ ///
/// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties).
///
/// Note that this function does not attempt to do any deep copying.
@@ -256,6 +286,11 @@ type DynamicObj() =
this.ShallowCopyDynamicPropertiesTo(target)
target
+ member this.DeepCopyDynamicProperties() =
+ let target = DynamicObj()
+ this.DeepCopyDynamicPropertiesTo(target)
+ target
+
#if !FABLE_COMPILER
// Some necessary overrides for methods inherited from System.Dynamic.DynamicObject()
//
diff --git a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
index 53a9307..9dd8ae7 100644
--- a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
+++ b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
@@ -8,6 +8,7 @@
+
diff --git a/tests/DynamicObject.Tests/DynamicObjs.fs b/tests/DynamicObject.Tests/DynamicObjs.fs
index 46aa66a..221c8d0 100644
--- a/tests/DynamicObject.Tests/DynamicObjs.fs
+++ b/tests/DynamicObject.Tests/DynamicObjs.fs
@@ -3,6 +3,7 @@
open System
open Fable.Pyxpecto
open DynamicObj
+open TestUtils
let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [
testCase "NonExisting" <| fun _ ->
@@ -485,6 +486,120 @@ let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties"
Expect.equal a b "copied value was not mutated via reference"
]
+let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
+
+ let constructClone (props: seq) =
+ let original = DynamicObj()
+ props
+ |> Seq.iter (fun (propertyName, propertyValue) -> original.SetProperty(propertyName, propertyValue))
+ let clone = original.DeepCopyDynamicProperties()
+ original, clone
+
+ let bulkMutate (props: seq) (dyn: DynamicObj) =
+ props |> Seq.iter (fun (propertyName, propertyValue) -> dyn.SetProperty(propertyName, propertyValue))
+
+ testList "DynamicObj" [
+ testList "Cloneable dynamic properties" [
+ testCase "primitives" <| fun _ ->
+ let originalProps = [
+ "int", box 1
+ "float", box 1.0
+ "bool", box true
+ "string", box "hello"
+ "char", box 'a'
+ "byte", box (byte 1)
+ "sbyte", box (sbyte -1)
+ "int16", box (int16 -1)
+ "uint16", box (uint16 1)
+ "int32", box (int32 -1)
+ "uint32", box (uint32 1u)
+ "int64", box (int64 -1L)
+ "uint64", box (uint64 1UL)
+ "single", box (single 1.0f)
+ "decimal", box (decimal 1M)
+ ]
+ let original, clone = constructClone originalProps
+ let mutatedProps = [
+ "int", box 2
+ "float", box 2.0
+ "bool", box false
+ "string", box "bye"
+ "char", box 'b'
+ "byte", box (byte 2)
+ "sbyte", box (sbyte -2)
+ "int16", box (int16 -2)
+ "uint16", box (uint16 2)
+ "int32", box (int32 -2)
+ "uint32", box (uint32 2u)
+ "int64", box (int64 -2L)
+ "uint64", box (uint64 2UL)
+ "single", box (single 2.0f)
+ "decimal", box (decimal 2M)
+ ]
+ bulkMutate mutatedProps original
+ Expect.notEqual original clone "Original and clone should not be equal after mutating primitive props on original"
+ Expect.sequenceEqual (original.GetProperties(true) |> Seq.map (fun p -> p.Key, p.Value)) mutatedProps "Original should have mutated properties"
+ Expect.sequenceEqual (clone.GetProperties(true) |> Seq.map (fun p -> p.Key, p.Value)) originalProps "Clone should have original properties"
+ testCase "DynamicObj" <| fun _ ->
+ let inner = DynamicObj() |> DynObj.withProperty "inner int" 2
+ let original, clone = constructClone ["dyn", inner]
+ inner.SetProperty("inner int", 1)
+ Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
+ Expect.equal (original |> DynObj.getNestedPropAs ["dyn";"inner int"]) 1 "Original should have mutated properties"
+ Expect.equal (clone |> DynObj.getNestedPropAs ["dyn";"inner int"]) 2 "Clone should have original properties"
+ testCase "Nested DynamicObj" <| fun _ ->
+ let first_level = DynamicObj() |> DynObj.withProperty "lvl1" 1
+ let second_level = DynamicObj() |> DynObj.withProperty "lvl2" 2
+ first_level.SetProperty("second_level", second_level)
+ let original, clone = constructClone ["first_level", first_level]
+ second_level.SetProperty("lvl2", -1)
+ Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
+ Expect.equal (original |> DynObj.getNestedPropAs ["first_level";"second_level";"lvl2"]) -1 "Original should have mutated properties"
+ Expect.equal (clone |> DynObj.getNestedPropAs ["first_level";"second_level";"lvl2"]) 2 "Clone should have original properties"
+ testCase "DynamicObj array" <| fun _ ->
+ let item1 = DynamicObj() |> DynObj.withProperty "item" 1
+ let item2 = DynamicObj() |> DynObj.withProperty "item" 2
+ let item3 = DynamicObj() |> DynObj.withProperty "item" 3
+ let arr = [|item1; item2; item3|]
+ let original, clone = constructClone ["arr", box arr]
+ item1.SetProperty("item", -1)
+ item2.SetProperty("item", -1)
+ item3.SetProperty("item", -1)
+ let originalProp = original |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ let clonedProp = clone |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
+ Expect.sequenceEqual originalProp [|-1; -1; -1|] "Original should have mutated properties"
+ Expect.equal clonedProp [|1; 2; 3|] "Clone should have original properties"
+ testCase "
+ ()
+ testCase "DynamicObj ResizeArray" <| fun _ ->
+ ()
+ ]
+ testList "Un-Cloneable dynamic properties" [
+ testCase "Class with mutable fields is reference equal" <| fun _ ->
+ ()
+ ]
+ ]
+ testList "Derived class" [
+ testList "Cloneable dynamic properties" [
+ testCase "primitives" <| fun _ ->
+ ()
+ testCase "DynamicObj" <| fun _ ->
+ ()
+ testCase "DynamicObj array" <| fun _ ->
+ ()
+ testCase "
+ ()
+ testCase "DynamicObj ResizeArray" <| fun _ ->
+ ()
+ ]
+ testList "Un-Cloneable dynamic properties" [
+ testCase "Class with mutable fields is reference equal" <| fun _ ->
+ ()
+ ]
+ ]
+]
+
let tests_Equals = testList "Equals" [
testCase "Same Object" <| fun _ ->
let a = DynamicObj()
@@ -583,6 +698,7 @@ let main = testList "DynamicObj (Class)" [
tests_GetProperties
tests_ShallowCopyDynamicPropertiesTo
tests_ShallowCopyDynamicProperties
+ tests_DeepCopyDynamicProperties
tests_Equals
tests_GetHashCode
]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/TestUtils.fs b/tests/DynamicObject.Tests/TestUtils.fs
new file mode 100644
index 0000000..caef957
--- /dev/null
+++ b/tests/DynamicObject.Tests/TestUtils.fs
@@ -0,0 +1,34 @@
+module TestUtils
+
+open DynamicObj
+
+let firstDiff s1 s2 =
+ let s1 = Seq.append (Seq.map Some s1) (Seq.initInfinite (fun _ -> None))
+ let s2 = Seq.append (Seq.map Some s2) (Seq.initInfinite (fun _ -> None))
+ Seq.mapi2 (fun i s p -> i,s,p) s1 s2
+ |> Seq.find (function |_,Some s,Some p when s=p -> false |_-> true)
+
+module DynObj =
+ let inline getNestedPropAs<'T> (propTree: seq) (dyn: DynamicObj) =
+ let props = propTree |> Seq.toList
+ let rec getProp (dyn: DynamicObj) (props: string list) : 'T=
+ match props with
+ | p::[] -> (dyn.GetPropertyValue(p)) |> unbox<'T>
+ | p::ps -> getProp (dyn.GetPropertyValue(p) |> unbox) ps
+ | _ -> failwith "Empty property list"
+ getProp dyn props
+
+module Expect =
+ /// Expects the `actual` sequence to equal the `expected` one.
+ let sequenceEqual actual expected message =
+ match firstDiff actual expected with
+ | _,None,None -> ()
+ | i,Some a, Some e ->
+ failwithf "%s. Sequence does not match at position %i. Expected item: %O, but got %O."
+ message i e a
+ | i,None,Some e ->
+ failwithf "%s. Sequence actual shorter than expected, at pos %i for expected item %O."
+ message i e
+ | i,Some a,None ->
+ failwithf "%s. Sequence actual longer than expected, at pos %i found item %O."
+ message i a
\ No newline at end of file