Reputation: 8116
I'm trying to build an Attribute that validates a certain instance of a type.
In order to do this I have to cast the ObjectInstance
to that type.
And I need to set the attribute on the member of that type.
So we need to resort to the and
keyword for the circular definition.
However in the following case I get the error that
A custom attribute must invoke an object constructor
On the line marked below.
namespace Test
open System
open System.ComponentModel.DataAnnotations
[<AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)>]
type MyAttribute() =
class
inherit ValidationAttribute ()
override this.IsValid (value: Object, validationContext: ValidationContext) =
match validationContext.ObjectInstance with
| :? MyClass as item ->
// TODO more validation
ValidationResult.Success
| _ ->
new ValidationResult("No no no")
end
and MyClass(someValue) =
[<Required>]
[<Range(1, 7)>]
//vvvvvvvvvvvvvvv
[<MyAttribute>]
//^^^^^^^^^^^^^^^
member this.SomeValue : int = someValue
I tried manually invoking the constructor, such as:
[<MyAttribute()>]
// or
[<new MyAttribute()>]
But none of them are accepted by the system.
Can an F# guru help me out here?
Upvotes: 9
Views: 524
Reputation: 66
One solution would be to first describe your types in a signature files.
Since the attribute is specified in the signature file, you don't need to add it again in the implementation file:
Foo.fsi:
namespace Foo
open System
[<AttributeUsage(AttributeTargets.Property)>]
type MyAttribute =
inherit System.Attribute
new : unit -> MyAttribute
member Foo : unit -> MyClass
and MyClass =
new : someValue : int -> MyClass
[<MyAttribute()>]
member SomeValue : int
Foo.fs:
namespace Foo
open System
[<AttributeUsage(AttributeTargets.Property)>]
type MyAttribute() =
inherit Attribute()
member this.Foo () =
new MyClass(1)
and MyClass(someValue) =
// [<MyAttribute()>] -> specified in the fsi, still appears in compiled code
member this.SomeValue : int = someValue
See https://msdn.microsoft.com/en-us/library/dd233196.aspx for reference
Upvotes: 3
Reputation: 13577
One thing that you can do to get rid of mutual recursion is to break up MyClass
definition into two and use type augmentation to add the members you want to mark with the attribute.
type MyClass(someValue: int) =
member internal this.InternalSomeValue = someValue
type MyAttribute() =
inherit ValidationAttribute()
(* you can refer to MyClass here *)
type MyClass with
[<MyAttribute()>]
member this.SomeValue = this.InternalSomeValue
That's closer to what you're asking for, but I like the interface idea better.
Upvotes: 3
Reputation: 3149
Interesting one. It seems that the type inference is really not getting that right. The correct syntax to use here is [<MyAttribute()>]
, but despite you using the and
keyword, the MyAttribute
class is not yet known.
Here is a workaround: First check that the object to validate is really of the right type, then use reflection to invoke a validation method:
[<AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)>]
type MyAttribute() =
inherit ValidationAttribute ()
override this.IsValid (value: Object, validationContext: ValidationContext) =
let t = validationContext.ObjectInstance.GetType()
if t.FullName = "Test.MyClass" then
let p = t.GetMethod("IsValid")
if p.Invoke(validationContext.ObjectInstance, [| |]) |> unbox<bool> then
ValidationResult.Success
else
ValidationResult("failed")
else
new ValidationResult("No no no")
type MyClass(someValue: int) =
[<Required>]
[<Range(1, 7)>]
[<MyAttribute()>]
member this.SomeValue = someValue
member this.IsValid() = someValue <= 7
Edit: to make that slightly cleaner, you could add an interface, that you use in your validation attribute, and later implement in your class.
type IIsValid =
abstract member IsValid: unit -> bool
Your IsValid method then becomes
override this.IsValid (value: Object, validationContext: ValidationContext) =
match validationContext.ObjectInstance with
| :? IIsValid as i ->
if i.IsValid() then
ValidationResult.Success
else
ValidationResult("failed")
| _ ->
ValidationResult("No no no")
in your class, this looks like:
type MyClass(someValue: int) =
[<Required>]
[<Range(1, 7)>]
[<MyAttribute()>]
member this.SomeValue = someValue
interface IIsValid with
member this.IsValid() = someValue <= 7
Upvotes: 7