Anemoia
Anemoia

Reputation: 8116

Circular reference and constructors

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

Answers (3)

Arnaud Rebts
Arnaud Rebts

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

scrwtp
scrwtp

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

Anton Schwaighofer
Anton Schwaighofer

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

Related Questions