Gotchas: Adding Attributes to Properties on Interfaces in F#
Tuesday, September 15, 2009 – 9:49 AMI ran into a couple of issues when writing an F# class to be consumed by C#. Specifically when it came to adding attributes to a property declared on an interface. You would expect the following code to decorate the SofteningLength property with the Dependency and DefaultValue attributes:
1: #light
2:
3: namespace NBody.DomainModel.Integrators
4:
5: open System
6: open System.ComponentModel
7: open NBody.DomainModel
8: open Microsoft.Practices.Unity
9:
10: [<Description("Forward Euler (F#)")>]
11: type public ForwardEulerFSharpIntegrator() = class
12: let mutable softeningLength = 0.0
13: let mutable isInitialized = false
14:
15: // IIntegrate implementation
16:
17: interface IIntegrate with
18:
19: [<property: Dependency("SofteningLength")>]
20: [<property: DefaultValue(0.01)>]
21: member public this.SofteningLength
22: with get() = softeningLength
23: and set(value) = softeningLength <- value
24:
25: member public this.Initialize(bodies) =
26: isInitialized <- true
27:
28: member public this.Integrate(dT, (bodies:Body[])) =
29: if not isInitialized then raise (InvalidOperationException("inner"))
30:
31: end
32: end
If it all worked I wouldn’t be writing this post. Turns out this is a bug in the F# May 2009 CTP (and probably Visual Studio 2010 Beta 1 & 2).
No attributes means that my code breaks are runtime. The Unity dependency injection container my application uses cannot create the object correctly as it doesn’t see the SofteningLength property as a dependency and other code I wrote to read the default value also fails to find the attribute. Looking at the IL or C# disassembly in Reflector shows that the attributes were not generated.
Workaround
Turns out that a subtle slight of hand can be used to “fix” the issue. Interfaces in F# are explicitly implemented. In C# the F# code I wrote above would look like this:
1: [Dependency("SofteningLength")]
2: [DefaultValue(0.01d)]
3: double IIntegrate.SofteningLength { get; set; }
This isn’t actually what I want. In most cases I’d be happy to have my interface implicitly implemented (the default in C#).
However, by implementing the same methods and properties on my ForwardEulerFSharpIntegrator type I can trick the calling code into finding the correct attributes:
1: #light
2:
3: namespace NBody.DomainModel.Integrators
4:
5: open System
6: open System.ComponentModel
7: open NBody.DomainModel
8: open Microsoft.Practices.Unity
9:
10: [<Description("Forward Euler (F#)")>]
11: type public ForwardEulerFSharpIntegrator() = class
12: let mutable softeningLength = 0.0
13: let mutable isInitialized = false
14:
15: // IIntegrate implementation
16:
17: interface IIntegrate with
18:
19: member public this.SofteningLength
20: with get() = softeningLength
21: and set(value) = softeningLength <- value
22:
23: member public this.Initialize(bodies) =
24: isInitialized <- true
25:
26: member public this.Integrate(dT, (bodies:Body[])) =
27: if not isInitialized then raise (InvalidOperationException("inner"))
28:
29: end
30:
31: // Implicit implementation of IIntegrate, also allows attributes to be set correctly
32:
33: [<property: Dependency("SofteningLength")>]
34: [<property: DefaultValue(0.01)>]
35: member public this.SofteningLength
36: with get() = (this :> IIntegrate).SofteningLength
37: and set(value) = (this :> IIntegrate).SofteningLength <- value
38:
39: member public this.Initialize(bodies) =
40: (this :> IIntegrate).Initialize(bodies)
41:
42: member public this.Integrate(dT, bodies) =
43: (this :> IIntegrate).Integrate(dT, bodies)
44:
45: end
In some cases I will call the SofteningLength property setter on the explicit interface but rely on the implicit implementation to get the attributes. Note how the implicit implementation casts (using the :> operator) to the interface and then calls the explicit implementation.
This seemed like it was worth writing up as I couldn’t find anyone else who’d blogged about it. I’ve also told the F# team so they’re aware of the issue.
2 Trackback(s)
Sorry, comments for this entry are closed at this time.