可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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]
回答2:
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