@@ -22,36 +22,61 @@ module internal Reflect =
22
22
23
23
let getProperties ( ty : Type ) = ty.GetRuntimeProperties() |> Seq.filter ( fun p -> let m = getPropertyMethod p in not m.IsStatic && m.IsPublic)
24
24
25
-
26
- let isCSharpRecordType ( ty : Type ) =
27
- let isInitOnly ( property : PropertyInfo ) =
25
+ let isInitOnlyProperty ( property : PropertyInfo ) =
28
26
#if NETSTANDARD1_ 0
29
- false
27
+ false
30
28
#else
31
- property.CanWrite
32
- && ( property.SetMethod.ReturnParameter.GetRequiredCustomModifiers()
33
- |> Array.exists ( fun t -> t.FullName = " System.Runtime.CompilerServices.IsExternalInit" ))
29
+ property.CanWrite
30
+ && ( property.SetMethod.ReturnParameter.GetRequiredCustomModifiers()
31
+ |> Array.exists ( fun t -> t.FullName = " System.Runtime.CompilerServices.IsExternalInit" ))
34
32
#endif
35
33
34
+ /// An immutable "record-like" type:
35
+ /// - must have a single public constructor
36
+ /// - must not have any property setters (not even C# init)
37
+ /// - must have all init-only fields (readonly in C# - these fields can only be set in the ctor)
38
+ /// These can be generated but not shrunk, because there is no reliable way to build a reader,
39
+ /// i.e. get the ctor parameter values back out from an existing value, because there's no
40
+ /// guarantee that the properties map back to the constructor arguments.
41
+ let isImmutableRecordLikeType ( ty : Type ) =
36
42
let typeinfo = ty.GetTypeInfo()
37
43
not typeinfo.IsAbstract
38
44
&& not typeinfo.ContainsGenericParameters
39
45
&& Seq.length ( getPublicCtors ty) = 1
40
- && not ( ty.GetRuntimeProperties() |> Seq.filter ( fun m -> not m.GetMethod.IsStatic && m.GetMethod.IsPublic) |> Seq.exists ( fun p -> p.CanWrite && not ( isInitOnly p ) ))
46
+ && not ( ty.GetRuntimeProperties() |> Seq.filter ( fun m -> not m.GetMethod.IsStatic && m.GetMethod.IsPublic) |> Seq.exists ( fun p -> p.CanWrite))
41
47
&& ty.GetRuntimeFields() |> Seq.filter ( fun m -> not m.IsStatic && m.IsPublic) |> Seq.forall ( fun f -> f.IsInitOnly)
42
48
43
- let isCSharpDtoType ( ty : Type ) =
49
+
50
+ /// A "C# record" type:
51
+ /// - must have a single public constructor
52
+ /// - must have a correspondingly named init-only property for every ctor argument
53
+ /// - may have additional settable properties (set or init)
54
+ /// This should cover types declared using C#'s record syntax, but also others.
55
+ /// As opposed to "immutable record-like" types, these can be shrunk automatically.
56
+ let isCSharpRecordType ( ty : Type ) =
44
57
let typeinfo = ty.GetTypeInfo()
45
- let hasOnlyDefaultCtor =
58
+ let props = getProperties ty |> Seq.toArray
59
+ let initOnlyPropNames =
60
+ props
61
+ |> Array.filter isInitOnlyProperty
62
+ |> Array.map ( fun p -> p.Name)
63
+ let hasRecordCtor =
64
+ // either no parameters, or for each parameter there is a corresponding
65
+ // init-only property which we can set. THis is what C# generates with its
66
+ // record syntax.
46
67
match getPublicCtors ty |> Array.ofSeq with
47
- [| ctor |] -> ctor.GetParameters() .Length = 0
68
+ [| ctor |] ->
69
+ ctor.GetParameters()
70
+ |> Seq.forall ( fun param -> initOnlyPropNames |> Array.contains param.Name)
48
71
| _ -> false
49
72
let hasWritableProperties =
50
- getProperties ty |> Seq .exists ( fun p -> p.CanWrite)
73
+ props |> Array .exists ( fun p -> p.CanWrite)
51
74
typeinfo.IsClass && not typeinfo.IsAbstract
52
75
&& not typeinfo.ContainsGenericParameters
53
- && hasOnlyDefaultCtor && hasWritableProperties
76
+ && hasRecordCtor
77
+ && hasWritableProperties
54
78
79
+ /// A collection type in the System.Collections.Immutable namespace.
55
80
let isImmutableCollectionType ( ty : Type ) =
56
81
ty.FullName.StartsWith( " System.Collections.Immutable" )
57
82
&& Array.contains ty.Name [|
@@ -61,30 +86,30 @@ module internal Reflect =
61
86
|]
62
87
63
88
64
- /// Get information on the fields of a record type
89
+ /// Get information on the fields of an F# record type
65
90
let getRecordFieldTypes ( recordType : System.Type ) =
66
91
if isRecordType recordType then
67
92
FSharpType.GetRecordFields( recordType, true )
68
93
|> Array.map ( fun pi -> pi.PropertyType)
69
94
else
70
- failwithf " The input type must be a record type. Got %A " recordType
95
+ failwithf " The input type must be an F# record type. Got %A " recordType
71
96
72
- /// Get constructor for record type
97
+ /// Get constructor for F# record type
73
98
let getRecordConstructor recordType =
74
99
FSharpValue.PreComputeRecordConstructor( recordType, true )
75
100
76
- /// Get reader for record type
101
+ /// Get reader for F# record type
77
102
let getRecordReader recordType =
78
103
FSharpValue.PreComputeRecordReader( recordType, true )
79
104
80
- let getCSharpRecordFields ( recordType : Type ) =
81
- if isCSharpRecordType recordType then
105
+ let getImmutableRecordLikeTypeFields ( recordType : Type ) =
106
+ if isImmutableRecordLikeType recordType then
82
107
let ctor = getPublicCtors recordType |> Seq.head
83
108
ctor.GetParameters() |> Seq.map ( fun p -> p.ParameterType)
84
109
else
85
110
failwithf " The input type must be an immutable class with a single constructor. Got %A " recordType
86
111
87
- let getCSharpRecordConstructor ( t : Type ) =
112
+ let getImmutableRecordLikeTypeConstructor ( t : Type ) =
88
113
let ctor = getPublicCtors t |> Seq.head
89
114
let ctorps = ctor.GetParameters ()
90
115
let par = Expression.Parameter ( typeof< obj[]>, " args" )
@@ -98,17 +123,28 @@ module internal Reflect =
98
123
let f = l.Compile ()
99
124
f.Invoke
100
125
101
- let getCSharpDtoFields ( recordType : Type ) =
102
- if isCSharpDtoType recordType then
126
+ let getCSharpRecordFields ( recordType : Type ) =
127
+ if isCSharpRecordType recordType then
103
128
getProperties recordType
104
129
|> Seq.filter ( fun p -> p.CanWrite)
105
130
|> Seq.map ( fun p -> p.PropertyType)
106
131
else
107
- failwithf " The input type must be a DTO class. Got %A " recordType
132
+ failwithf " The input type must be a C# record-like class. Got %A " recordType
108
133
109
- let getCSharpDtoConstructor ( t : Type ) =
134
+ let getCSharpRecordConstructor ( t : Type ) =
135
+ let props = getProperties t |> Seq.filter ( fun p -> p.CanWrite) |> Seq.toArray
136
+ let propNames = props |> Array.map ( fun p -> p.Name)
137
+
138
+ let ctor = getPublicCtors t |> Seq.head
139
+ let ctorps = ctor.GetParameters ()
140
+ let ctorParamPropIndex =
141
+ ctorps
142
+ |> Array.map ( fun ctorParam -> propNames |> Array.findIndex ( fun propName -> propName = ctorParam.Name))
110
143
let par = Expression.Parameter ( typeof< obj[]>, " args" )
111
- let props = getProperties t |> Seq.filter ( fun p -> p.CanWrite)
144
+ let pars = ctorps |> Array.mapi ( fun i p -> Expression.Convert (
145
+ Expression.ArrayIndex ( par, Expression.Constant ctorParamPropIndex.[ i]),
146
+ p.ParameterType)
147
+ :> Expression)
112
148
let values =
113
149
props
114
150
|> Seq.mapi ( fun i p ->
@@ -118,22 +154,23 @@ module internal Reflect =
118
154
props
119
155
|> Seq.zip values
120
156
|> Seq.map ( fun ( v , p ) -> Expression.Bind( p, v) :> MemberBinding)
121
- let ctor = Expression.New( getPublicCtors t |> Seq.head)
157
+
158
+ let ctor = Expression.New ( ctor, pars)
122
159
let body = Expression.MemberInit( ctor, bindings)
123
160
let l = Expression.Lambda< Func< obj[], obj>> ( body, par)
124
161
let f = l.Compile ()
125
162
f.Invoke
126
163
127
- let getCSharpDtoReader ( recordType : Type ) =
128
- if isCSharpDtoType recordType then
164
+ let getCSharpRecordReader ( recordType : Type ) =
165
+ if isCSharpRecordType recordType then
129
166
let properties = getProperties recordType
130
167
|> Seq.filter ( fun p -> p.CanWrite)
131
168
|> Seq.map ( fun p -> p.GetValue)
132
169
|> Seq.toArray
133
170
let lookup o = Array.map ( fun f -> f o) properties
134
171
lookup
135
172
else
136
- failwithf " The input type must be a DTO class. Got %A " recordType
173
+ failwithf " The input type must be a C# record-like class. Got %A " recordType
137
174
138
175
/// Returns the case name, type, and functions that will construct a constructor and a reader of a union type respectively
139
176
let getUnionCases unionType : ( string * ( int * System.Type list * ( obj [] -> obj ) * ( obj -> obj []))) list =
0 commit comments