1

I would like to create a metadata attribute called RangeAttribute without external tools like PostSharp as seen in this answer because it requires the paid version of the library.

The only official information that I've found about this is this MSDN documentation, but absurdly that page only explains how to declare the class and the inheritance... NOTHING more, so I'm more than lost.

My intention is to transform this code:

Public NotInheritable Class MyType

''' <summary>
''' Gets or sets the value.
''' </summary>
''' <value>The value.</value>
Public Property MyProperty As Integer
    Get
        Return Me._MyValue
    End Get
    Set(ByVal value As Integer)

        If value < Me._MyValueMin Then
            If Me._MyValueThrowRangeException Then
                Throw New ArgumentOutOfRangeException("MyValue", Me._MyValueExceptionMessage)
            End If
            Me._MyValue = Me._MyValueMin

        ElseIf value > Me._MyValueMax Then
            If Me._MyValueThrowRangeException Then
                Throw New ArgumentOutOfRangeException("MyValue", Me._MyValueExceptionMessage)
            End If
            Me._MyValue = Me._MyValueMax

        Else
            Me._MyValue = value

        End If

    End Set
End Property
Private _MyValue As Integer = 0I
Private _MyValueMin As Integer = 0I
Private _MyValueMax As Integer = 10I
Private _MyValueThrowRangeException As Boolean = True
Private _MyValueExceptionMessage As String = String.Format("The valid range is beetwen {0} and {1}",
                                                           Me._MyValueMin, Me._MyValueMax)

End Class

Into something reusable and simplified, like this:

Public NotInheritable Class MyType

    ''' <summary>
    ''' Gets or sets the value.
    ''' Valid range is between 0 and 10.
    ''' </summary>
    ''' <value>The value.</value>
    <RangeAttribute(0, 10, ThrowRangeException:=False, ExceptionMessage:="")>
    Public Property MyProperty As Integer

End Class

So to accomplish this task I've started writting the attribute, but is uncomplete due to an insufficient documentation or examples then I don't know how to procceed to evaluate the values in the setter of a property without adding manually the getter/setter in the property of the code above:

<AttributeUsage(AttributeTargets.Property Or
                AttributeTargets.Parameter Or
                AttributeTargets.ReturnValue Or
                AttributeTargets.Field, 
                AllowMultiple:=False)>
Public Class RangeAttribute : Inherits Attribute

    ''' <summary>
    ''' Indicates the Minimum range value.
    ''' </summary>
    Public Minimum As Single

    ''' <summary>
    ''' Indicates the Maximum range value.
    ''' </summary>
    Public Maximum As Single

    ''' <summary>
    ''' Determines whether to throw an exception when the value is not in range.
    ''' </summary>
    Public ThrowRangeException As Boolean

    ''' <summary>
    ''' Indicates the exception message to show when the value is not in range.
    ''' </summary>
    Public ExceptionMessage As String

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RangeAttribute"/> class.
    ''' </summary>
    ''' <param name="Minimum">The minimum range value.</param>
    ''' <param name="Maximum">The maximum range value.</param>
    Public Sub New(ByVal Minimum As Single,
                   ByVal Maximum As Single)

        Me.New(Minimum, Maximum, ThrowRangeException:=False, ExceptionMessage:=String.Empty)

    End Sub

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RangeAttribute"/> class.
    ''' </summary>
    ''' <param name="Minimum">The minimum range value.</param>
    ''' <param name="Maximum">The maximum range value.</param>
    ''' <param name="ThrowRangeException">
    ''' Determines whether to throw an exception when the value is not in range.
    ''' </param>
    Public Sub New(ByVal Minimum As Single,
                   ByVal Maximum As Single,
                   ByVal ThrowRangeException As Boolean,
                   Optional ByVal ExceptionMessage As String = "")

        Me.Minimum = Minimum
        Me.Maximum = Maximum
        Me.ThrowRangeException = ThrowRangeException

        If Not String.IsNullOrEmpty(ExceptionMessage) Then
            Me.ExceptionMessage = ExceptionMessage
        Else
            Me.ExceptionMessage = String.Format("The valid range is beetwen {0} and {1}", Minimum, Maximum)
        End If

    End Sub

End Class

The Attribute code above will ignore the values that are not in range, i understand this is because I'm not evaluating nothing, but I don't know how to do it.

Community
  • 1
  • 1
ElektroStudios
  • 19,105
  • 33
  • 200
  • 417
  • [Fody](https://github.com/Fody/PropertyChanged) does something like this similar. It injects INotifyPropertyChanged code at compile time. Feel free to look in to the source and see if that can help you. – Sriram Sakthivel Oct 01 '14 at 19:33

2 Answers2

3

Well there are other AOP frameworks/libraries available for .Net platform, Spring.net AOP, KingAOP, FluentAOP, Afterthought,... to name a few.

Here is a proposed solution using Afterthought.

NOTE: We can divide AOP frameworks to two major categories based on techniques used for interception, frameworks which inject the interception code during Compile-time (Compile-time IL weaving) and the ones which do the injection during run-time (run-time IL Weaving or Dynamic IL-weaving). PostSharp supports both methods in current version, each technique has its own pros and cons which is out of scope of this answer, for more information you can refer to http://www.postsharp.net/aop.net

In this sample we chose Compile-time IL-Weaving based on Afterthought framework (Afterthought only supports compile-time IL weaving)

1-Prepration

you can get Afterthought from https://github.com/r1pper/Afterthought/releases (you can download the binaries or you can get the source and compile it by yourself, I go the binary route here)

extract the package there are 2 files Afterthought.dll and Afterthought.Amender.exe, reference to afterthought.dll.

As I said before Afterthought uses compile-time IL weaving and this is exactly what Afterthought.Amender.exe does.

we should call Amender after each build to inject the interception code to our assembly:

Afterthought.Amender.exe "assembly"

we can automate the task by defining a new Post Build event for our project (this is exactly what PostSharp does) Here I copied Afterthought folder in my project's directory and this is my post build event(you may need to change the post event based on your folder location):

"$(ProjectDir)Afterthought\Afterthought.Amender.exe" "$(TargetPath)"

OK, now we are ready to write our code

2- Sample Code with Range control for Integer numbers between [0,10]

In this sample we define a range control attribute and name it RangeAttribute an try to intercept properties setter method to check if our set value is within the range.

Interception Code and Injection:

Imports Afterthought
Imports System.Reflection

Public Class RangeAmendment(Of T)
    Inherits Amendment(Of T, T)
    Public Sub New()
        MyBase.New()
        Console.WriteLine("Injecting range check here!")

        Properties.AfterSet(Sub(instance As T, pName As String, pvOld As Object, pv As Object, pvNew As Object)

                                Dim p As PropertyInfo = instance.GetType().GetProperty(pName)
                                Dim att As RangeAttribute = p.GetCustomAttribute(Of RangeAttribute)()
                                If att Is Nothing Then Return

                                Dim v As Object = p.GetValue(instance)
                                Dim castedValue As Integer = Convert.ToInt32(v)
                                If (castedValue < att.Min OrElse castedValue > att.Max) Then
                                    Throw New RangeException(p.Name, att.Min, att.Max)
                                End If

                            End Sub)
    End Sub
End Class

Classes and Definitions:

Public Class RangeAttribute
    Inherits Attribute

    Public Property Max As Integer

    Public Property Min As Integer

    Public Sub New(ByVal min As Integer, ByVal max As Integer)
        MyBase.New()
        Me.Min = min
        Me.Max = max
    End Sub
End Class

Public Class RangeException
    Inherits ApplicationException
    Public Sub New(ByVal propertyName As String, ByVal min As Integer, ByVal max As Integer)
        MyBase.New(String.Format("property '{0}' value should be between [{1},{2}]", propertyName, min, max))
    End Sub
End Class



<Amendment(GetType(RangeAmendment(Of )))>
Public Class TestClass
    <Range(0, 10)>
    Public Property Value As Integer

    Public Sub New()
        MyBase.New()
    End Sub
End Class

Sample:

Module Module1

        Sub Main()
            Dim test = New TestClass()
    
            Try
                Console.WriteLine("try setting value to 5")
                test.Value = 5
                Console.WriteLine(test.Value)
    
                Console.WriteLine("try setting value to 20")
                test.Value = 20
                Console.WriteLine(test.Value)
    
            Catch ex As RangeException
                Console.WriteLine(ex.Message)
            End Try
    
            Console.ReadKey()
        End Sub
    
    End Module

now when you build your project you should see similar message in your build output:

Injecting range check here!

Amending AopVb3.exe (3.685 seconds)

========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

and the output of the console should be :

try setting value to 5

5

try setting value to 20

property 'Value' value should be between [0,10]

Community
  • 1
  • 1
user3473830
  • 7,165
  • 5
  • 36
  • 52
1

New, revised, updated answer; also deleted my comments below:

Ok, here is something which can work and is not horribly intrusive. First, a few words about what you have been looking at.

Attributes provide meta data for a Type or Property etc. Since it is compiled into the final assembly, the only way to get at them is via Reflection. You cant just add an Attribute to something and have it magically do something without a some code somewhere to activate the methods etc in it. The attribute itself then has to use Reflection to determine which Type and property it is associated with. In some cases, there is an entire library to support these activities.

The NET Range you looked at a few days back is much more involved than it appears. Note that there is no Validate or CheckValue type method, just a Boolean IsValid! Aside from being a Web-Thing, it also appears to be related to databinding - there is also a RangeAttributeAdapter, a ValidationArttribute (Range inherits from this) and ValidationContext. The RangeAttribute is simply where the values are specified and there is much, much more going on than just a simple attribute.

Other things, like PostSharp are "Weavers" - simple enough to use (kind of), but they rewrite your code to inject what amounts to wrappers to monitor property changes and call your range validation method(s). Then more reflection to post the validated data back to the property.

The point: none of the things you have looked at are just Attributes, there is much more going on.

The following is a RangeManager which isn't as transparent as a Weaver but it is simpler. At the heart is a Range Attribute where you can specify the valid min/max. But there is also a RangeManager object you need to create which will do the heavy lifting of finding the property, converting from setter method, finding the valid range and then testing it. It scours the type it is instanced in to find all the relevant properties.

It is frugal on the Reflection calls. When instanced, the Manager finds all the tagged properties and saves a reference to the RangeAttribute instance so that every time you set a property there are not several new Reflection methods invoked.

If nothing else it shows a little of what is involved.

Imports System.Reflection
Imports System.Globalization

Public Class RangeManager

    <AttributeUsage(AttributeTargets.Property)>
    Public Class RangerAttribute
        Inherits Attribute

        Public Property Minimum As Object
        Public Property Maximum As Object
        Private min As IComparable
        Private max As IComparable

        ' converter: used by IsValid which is not overloaded
        Private Property Conversion() As Func(Of Object, Object)

        Public Property VarType As Type

        Public Sub New(n As Integer, x As Integer)
            Minimum = n
            Maximum = x
            VarType = GetType(Integer)
            min = CType(Minimum, IComparable)
            max = CType(Maximum, IComparable)
            Conversion = Function(v) Convert.ToInt32(v, 
                         CultureInfo.InvariantCulture)
        End Sub

        Public Sub New(n As Single, x As Single)
            Minimum = n
            Maximum = x
            VarType = GetType(Single)
            min = CType(Minimum, IComparable)
            max = CType(Maximum, IComparable)
            Conversion = Function(v) Convert.ToSingle(v,
                         CultureInfo.InvariantCulture)
        End Sub

        Public Sub New(n As Double, x As Double)
            Minimum = n
            Maximum = x
            VarType = GetType(Double)
            min = CType(Minimum, IComparable)
            max = CType(Maximum, IComparable)
            Conversion = Function(v) Convert.ToDouble(v, 
                         CultureInfo.InvariantCulture)
        End Sub

        ' overridable so you can inherit and provide more complex tests
        ' e.g. String version might enforce Casing or Length
        Public Overridable Function RangeCheck(value As Integer) As Integer
            If min.CompareTo(value) < 0 Then Return CInt(Minimum)
            If max.CompareTo(value) > 0 Then Return CInt(Maximum)
            Return value
        End Function

        Public Overridable Function RangeCheck(value As Single) As Single
            If min.CompareTo(value) < 0 Then Return CSng(Minimum)
            If max.CompareTo(value) > 0 Then Return CSng(Maximum)
            Return value
        End Function

        Public Overridable Function RangeCheck(value As Double) As Double
            If min.CompareTo(value) < 0 Then Return CDbl(Minimum)
            If max.CompareTo(value) > 0 Then Return CDbl(Maximum)
            Return value
        End Function

        ' rather than throw exceptions, provide an IsValid method
        ' lifted from MS Ref Src
        Public Function IsValid(value As Object) As Boolean
            ' dont know the type
            Dim converted As Object

            Try
                converted = Me.Conversion(value)
            Catch ex As InvalidCastException
                Return False
            Catch ex As NotSupportedException
                Return False

                ' ToDo: add more Catches as you encounter and identify them
            End Try

            Dim min As IComparable = CType(Minimum, IComparable)
            Dim max As IComparable = CType(Maximum, IComparable)

            Return min.CompareTo(converted) <= 0 AndAlso 
                          max.CompareTo(converted) >= 0
        End Function

    End Class

    ' map of prop names to setter method names
    Private Class PropMap
        Public Property Name As String      ' not critical - debug aide
        Public Property Setter As String

        ' store attribute instance to minimize reflection
        Public Property Range As RangerAttribute

        Public Sub New(pName As String, pSet As String, r As RangerAttribute)
            Name = pName
            Setter = pSet
            Range = r
        End Sub
    End Class


    Private myType As Type             ' not as useful as I'd hoped
    Private pList As List(Of PropMap)

    Public Sub New()
        ' capture calling Type so it does not need to be specified
        Dim frame As New StackFrame(1)
        myType = frame.GetMethod.DeclaringType

        ' create a list of Props and their setter names
        pList = New List(Of PropMap)

        BuildPropMap()
    End Sub

    Private Sub BuildPropMap()
        ' when called from a prop setter, StackFrame reports
        ' the setter name, so map these to the prop name

        Dim pi() As PropertyInfo = myType.GetProperties

        For Each p As PropertyInfo In pi
            ' see if this prop has our attr
            Dim attr() As RangerAttribute =
                DirectCast(p.GetCustomAttributes(GetType(RangerAttribute), True), 
                                    RangerAttribute())

            If attr.Count > 0 Then
                ' find it
                For n As Integer = 0 To attr.Count - 1
                    If attr(n).GetType = GetType(RangerAttribute) Then
                        pList.Add(New PropMap(p.Name, p.GetSetMethod.Name, attr(n)))
                        Exit For
                    End If
                Next
            End If

        Next

    End Sub

    ' can be invoked only from Setter!
    Public Function IsValid(value As Object) As Boolean
        Dim frame As New StackFrame(1)
        Dim pm As PropMap = GetPropMapItem(frame.GetMethod.Name)
        Return pm.Range.IsValid(value)
    End Function

    ' validate and force value to a range
    Public Function CheckValue(value As Integer) As Integer
        Dim frame As New StackFrame(1)
        Dim pm As PropMap = GetPropMapItem(frame.GetMethod.Name)

        If pm IsNot Nothing Then
            Return pm.Range.CheckValue(value)
        Else
            Return value      ' or something else
        End If

    End Function

    ' other types omitted for brevity:   
    Public Function CheckValue(value As Double) As Double
       ...
    End Function

    Public Function CheckValue(value As Single) As Single
       ...
    End Function

    Private Function GetPropMapItem(setterName As String) As PropMap
        For Each p As PropMap In pList
            If p.Setter = setterName Then
                Return p
            End If
        Next
        Return Nothing
    End Function

End Class

As noted in the code comments, you could inherit RangerAttribute so you could provide more extensive range tests.

Sample usage:

Imports RangeManager

Public Class FooBar

    Public Property Name As String

    Private _IntVal As Integer
    <Ranger(1, 10)>
    Public Property IntValue As Integer
        Get
            Return _IntVal
        End Get
        Set(value As Integer)
            _IntVal = rm.CheckValue(value)
        End Set
    End Property  

    ' this is a valid place to use Literal type characters
    ' to make sure the correct Type is identified
    Private _sngVal As Single
    <Ranger(3.01F, 4.51F)>
    Public Property SngValue As Single
        Get
            Return _sngVal
        End Get
        Set(value As Single)
            If rm.IsValid(value) = False Then
                Console.Beep()
            End If
            _sngVal = rm.CheckValue(value)
        End Set
    End Property

    Private rm As RangeManager

    Public Sub New(sName As String, nVal As Integer, dVal As Decimal)
        ' rm is mainly used where you want to validate values
        rm = New RangeManager

        ' test if this can be used in the ctor
        Name = sName
        IntValue = nVal * 100
        DblValue = dVal

    End Sub

End Class

Test Code:

Dim f As New FooBar("ziggy", 1, 3.14)

f.IntValue = 900
Console.WriteLine("val tried: {0} result: {1}", 900.ToString, f.IntValue.ToString)

f.IntValue = -23
Console.WriteLine("val tried: {0} result: {1}", (-23).ToString, f.IntValue.ToString)


f.SngValue = 98.6
Console.WriteLine("val tried: {0} result: {1}", (98.6).ToString, f.SngValue.ToString)

There you have it: 220 lines of code for an Attribute based range validator to replace the following in your setters:

If value < Minimum Then value = Minimum
If value > Maximum Then value = Maximum

To me, the only thing which gets it past my gag factor as far as offloading data validation to something outside the property and class is that the ranges used are listed right there above the property.


Attributes know nothing about the Properties they decorate. It is up to Something Else to make that connection, and that Something Else will need to use Reflection to get at the Attribute data.

Likewise, Properties know nothing about the Attributes assigned to them because the Attributes are metadata intended for the compiler or Something Else like a serializer. This Something Else must also use Reflection to make a connection between the two tiers (Type methods and meta data).

In the end, Something Else ends up being either a tool to rewrite your emitted assembly in order to provide the range checking service, or a library to provide the service via a method as shown above.

The hurdle to something being more transparent is that there is not something like a PropertyChanged event to hook onto (see PropertyInfo).

  • Implement IComparable for use in RangeCheck
Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
  • `I dont think you want to be throwing exceptions when the value is out of range` Why not? in case of debugging or to discover a failling with one of the ranges could be veru useful, the `RangeAttribute` for ASP.Net has the a `Throwexception` feature (I don't remember the exact parameter name) which can be set optionally with an optional exception message. – ElektroStudios Oct 01 '14 at 21:26
  • About the code, first of all Thankyou, but this is exactlly what I would like to avoid, my intention is to simplify the properties to avoid writting a getter/setter then just attach the range attribute. if you note it the first code that I shown and your last code does basically the same, mine without reflection and yours with reflection, I really don't see any beneffit on reflection approach, you are evaluating the values in the setter, is the same thing but made ​in a more heavy way. thanks for your answer – ElektroStudios Oct 01 '14 at 21:28
  • see please the solution in this question using `PostSharp`: http://stackoverflow.com/questions/20019130/minvalue-maxvalue-attribute-for-properties as you see the only requeriment in the property is "attach" the range attribute, no getter and no setter, I don't have any idea about what the hell does the `LocationInterceptionAspect` or how I can do something similar for this, but that means that attributes `yes can do something`, then a getter/setter is not necessary once discovered how to only add the range attrib in top of the property (and the class for the range attribute). thanks – ElektroStudios Oct 01 '14 at 21:44
  • I understand that `PostSharp` is written by a group of professional developers with thousands and thousands of coded lines, but I don't pretend to mimic the funcionality of the whole library, I'm just mentioning that specific functionality to give a real example which demonstrates that to implement a `RangeAttribute` is not necessary still evaluating manually the values writting a getter/setter. – ElektroStudios Oct 01 '14 at 21:59
  • In the other hand,I never imagined that reflection should be necessary to realize this,loading the assembly each time that a property is set seems not the best thing about performance,but anyways if its neccesary to implement a RangeAttibute then I do not care much about its usage, what I would like to do is just add my custom attrib over the property declaration and then manage all necessary things from the Range related classes like evaluations and whatever is needed, but not doing all that inside the property getter/setter and merge more code, could you help me to realize something similar? – ElektroStudios Oct 01 '14 at 22:06