Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions DynamicObj.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31515.178
# Visual Studio Version 17
VisualStudioVersion = 17.0.31521.260
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this has any effect, but this is from VS Preview right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I will revert it once we understand that everything else is good enough

MinimumVisualStudioVersion = 10.0.40219.1
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "DynamicObj", "src\DynamicObj\DynamicObj.fsproj", "{B8BF1554-AAC3-434E-9502-FC83B43F3704}"
EndProject
Expand Down Expand Up @@ -38,6 +38,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{42AA66FC-8
docs\index.fsx = docs\index.fsx
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{988D804A-3A42-4E46-B233-B64F5C22524B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -52,10 +56,18 @@ Global
{D009964D-9408-4344-B610-B73F54FE2A86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D009964D-9408-4344-B610-B73F54FE2A86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D009964D-9408-4344-B610-B73F54FE2A86}.Release|Any CPU.Build.0 = Release|Any CPU
{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D009964D-9408-4344-B610-B73F54FE2A86} = {988D804A-3A42-4E46-B233-B64F5C22524B}
{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D} = {988D804A-3A42-4E46-B233-B64F5C22524B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6F5C3597-4524-4A4E-94EC-44857BD0BCEC}
EndGlobalSection
Expand Down
2 changes: 2 additions & 0 deletions src/DynamicObj/DynamicObj.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@

<ItemGroup>
<Compile Include="ReflectionUtils.fs" />
<Compile Include="ImmutableDynamicObj.fs" />
<Compile Include="DynamicObj.fs" />
<Compile Include="DynObj.fs" />
<Compile Include="Operators.fs" />
<None Include="Playground.fsx" />
</ItemGroup>

Expand Down
105 changes: 105 additions & 0 deletions src/DynamicObj/ImmutableDynamicObj.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
namespace DynamicObj

open DynamicObj
open System.Reflection
open System.Runtime.CompilerServices

[<InternalsVisibleToAttribute("UnitTests")>]
do()

/// Represents an DynamicObj's counterpart
/// with immutability enabled only.
type ImmutableDynamicObj internal (map : Map<string, obj>) =

let mutable properties = map

member private this.Properties
with get () =
properties
and set value =
properties <- value

// Copies the fields of one object to the other one
// If their base is not ImmutableDynamicObj, then it
// will go over fields from the base instance
static member private copyMembers (ty : System.Type) sourceObject destinationObject =
for fi in ty.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic ||| BindingFlags.Public) do
let fieldValue = fi.GetValue sourceObject
fi.SetValue(destinationObject, fieldValue)

static member private newIfNeeded (object : 'a when 'a :> ImmutableDynamicObj) map : 'a =
if obj.ReferenceEquals(map, object.Properties) then
object
else
// otherwise we create a new instance
let res = new 'a()

// and then copy all current fields the new instance
ImmutableDynamicObj.copyMembers (typeof<'a>) object res
res.Properties <- map
res

/// Empty instance
new () = ImmutableDynamicObj Map.empty

static member empty = ImmutableDynamicObj ()

/// Indexes ; if no key found, throws
member this.Item
with get(index) =
this.Properties.[index]

/// Returns an instance with:
/// 1. this property added if it wasn't present
/// 2. this property updated otherwise
static member add name newValue (object : #ImmutableDynamicObj) =
object.Properties
|> Map.add name newValue
|> ImmutableDynamicObj.newIfNeeded object

/// Returns an instance:
/// 1. the same if there was no requested property
/// 2. without the requested property if there was
static member remove name (object : #ImmutableDynamicObj) =
object.Properties
|> Map.remove name
|> ImmutableDynamicObj.newIfNeeded object

member this.TryGetValue name =
match this.Properties.TryGetValue name with
| true, value -> Some value
| _ -> ReflectionUtils.tryGetPropertyValue this name

member this.TryGetTypedValue<'a> name =
match this.TryGetValue name with
| None -> None
| Some o ->
match o with
| :? 'a as o -> o |> Some
| _ -> None

override this.Equals o =
match o with
| :? ImmutableDynamicObj as other -> other.Properties = this.Properties
| _ -> false

override this.GetHashCode () = ~~~map.GetHashCode()

[<Extension>]
type ImmutableDynamicObjExtensions =

/// Returns an instance with:
/// 1. this property added if it wasn't present
/// 2. this property updated otherwise
/// use this one only from C#
[<Extension>]
static member AddItem (this, name, newValue) =
ImmutableDynamicObj.add name newValue this

/// Returns an instance:
/// 1. the same if there was no requested property
/// 2. without the requested property if there was
/// use this one only from C#
[<Extension>]
static member RemoveItem (this, name) =
ImmutableDynamicObj.remove name this
29 changes: 29 additions & 0 deletions src/DynamicObj/Operators.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module DynamicObj.Operators



/// Returns an instance with:
/// 1. this property added if it wasn't present
/// 2. this property updated otherwise
let (++) object (name, newValue) = ImmutableDynamicObj.add name newValue object


/// Returns an instance:
/// 1. the same if there was no requested property
/// 2. without the requested property if there was
let (--) object name = ImmutableDynamicObj.remove name object


/// Acts as (++) if the value is Some,
/// returns the same object otherwise
let (++?) object (name, newValue) =
match newValue with
| Some(value) -> object ++ (name, value)
| None -> object

/// Acts as (++?) but maps the valid value
/// through the last argument
let (++??) object (name, newValue, f) =
match newValue with
| Some(value) -> object ++ (name, f value)
| None -> object
26 changes: 26 additions & 0 deletions tests/CSharpTests/CSharpTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DynamicObj\DynamicObj.fsproj" />
</ItemGroup>

</Project>
58 changes: 58 additions & 0 deletions tests/CSharpTests/InteropWorks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using Xunit;
using DynamicObj;
using Microsoft.FSharp.Collections;
using System.Collections.Generic;
using System.Linq;

namespace CSharpTests
{
public class InteropWorks
{
[Fact]
public void Test1()
{
var obj1 =
new ImmutableDynamicObj()
.AddItem("aa", 4)
.AddItem("bb", 10)
.RemoveItem("aa")
;
var obj2 =
new ImmutableDynamicObj()
.AddItem("bb", 10)
;
Assert.Equal(obj1, obj2);
}

public class MyDynamicObject : ImmutableDynamicObj
{

}

[Fact]
public void Test2()
{
var obj1 =
new MyDynamicObject()
.AddItem("aaa", 5);
Assert.IsType<MyDynamicObject>(obj1);
Assert.Equal(5, obj1["aaa"]);
}

public class MyDynamicObjectWithField : ImmutableDynamicObj
{
public int Aaa { get; init; }
}

[Fact]
public void TestFieldsPreservedForInheritor()
{
var obj1 =
new MyDynamicObjectWithField() { Aaa = 100500 }
.AddItem("aaa", 5)
.RemoveItem("aaa");
Assert.Equal(100500, obj1.Aaa);
}
}
}
Loading