【问题标题】:How can I dynamically make entity properties read-only?如何动态地将实体属性设为只读?
【发布时间】:2013-11-04 11:24:24
【问题描述】:

我正在使用 EF 4.5 和 DbContext。在业务规则层级别,我应该实施检查以避免在某些实体场景中更改实体值属性。示例:StartProjecteDateProjectIsStarted 时应该是只读的,但不是其他状态。

我遵循 DRY 原则,因此,我应该能够从上下文和 UI 中检查只读属性列表。

我的问题:

是否有可以将属性动态设置为只读的 DataAnnotation 验证器?

(如果没有,这个问题是否有不同/更好的解决方案?)

请注意,我正在使用 Web 表单(和 Telerik)架构,欢迎使用干净优雅的模式。

正如Jesse Webb 解释的那样,我正在尝试在运行时设置和获取 EditableAttribute,但我无法从属性中获取数据注释属性,我的代码:

<EditableAttribute(False)>
<MaxLength(400, ErrorMessage:="Màxim 400 caracters")>
Public Property NomInvertebrat As String

2013 年 11 月 8 日编辑 在挖掘文档后,似乎数据注释是针对类但例如对象本身。也许 iReadonlyableProperties 接口可能是一种方式。

【问题讨论】:

  • 是我(我已经重做了)。为什么?因为我认为这是某种“请为我做”的问题。 “有关您编写的代码问题的问题必须在问题本身中描述具体问题 - 并包含重现它的有效代码。”
  • 但我认为这个问题不会被关闭,因为它是一个赏金问题,而且你有超过 10K 的代表 :)
  • 一种解决方案是,您的实体不使用属性,而是实现 System.ComponentModel.DataAnnotations.IValidatableObject,然后 EF 将在调用 SaveChanges 时自动调用其 Validate 方法,您可以在那里实现任何自定义验证...跨度>
  • @nemesv,感谢您的评论。我已经覆盖了context.validateEntity,并且我有自己的验证。此外,对象被注释。但是,如您所知,EditableAttribute 不会引发任何异常,它仅供参考。
  • 你可以通过实现CustomTypeDescriptor来做到这一点,更多细节你可以看这个项目entityrestsdk.codeplex.com,免责声明,我是这个项目的作者。

标签: entity-framework data-annotations dbcontext


【解决方案1】:

我有一个包含扩展方法的类,可以让我像这样读取数据注释:

int maxRefLen = ReflectionAPI.GetProperty<Organisation, String>(x => x.Name)
                             .GetAttribute<StringLengthAttribute>()
                             .GetValueOrDefault(x => x.MaximumLength, 256);

因此,如果您使用它,您应该能够像这样获取EditableAttribute 的值:

bool isEditable = ReflectionAPI.GetProperty<Foo, String>(x => x.NomInvertebrat)
                               .GetAttribute<EditableAttribute>()
                               .GetValueOrDefault(x => x.AllowEdit, true);

至于在运行时设置数据注释,我自己没有做过,但我读到这里有一个解决方案:Setting data-annotations at runtime

获取我认为需要reading the entity framework metadata 的特定类型的所有数据注释的列表。同样,我还没有尝试过。

如果你把它加在一起,我个人认为它感觉笨重而不是优雅,但你已经要求使用DataAnnotations 的解决方案,更优雅的东西可能意味着进入你的架构。

我倾向于这样做:

public bool StartDateIsReadOnly
{
   //use this property client-side to disable the input
   get{ return Project.IsStarted;}
}

//Implement IValidatable object to do server side validation
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext
{
   bool startdateIsChanged = // I'll leave you to work out this bit
   var results = new List<ValidationResult>();
   if(StartDateIsReadOnly && startdateIsChanged)
   results.Add(new ValidationResult("Start Date cannot be changed after project is started");
}

这里是 ReflectionAPI 类:

请注意,该课程包含@JonSkeet 发布并描述为“邪恶”的部分黑客攻击。我个人认为这一点还不错,但您应该阅读以下参考资料:

Override a generic method for value types and reference types.

Evil code - overload resolution workaround

public static class ReflectionAPI
{

    public static int GetValueOrDefault<TInput>(this TInput a, Func<TInput, int> func, int defaultValue)
        where TInput : Attribute
    //Have to restrict to struct or you get the error:
    //The type 'R' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'System.Nullable<T>'
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    public static Nullable<TResult> GetValueOrDefault<TInput, TResult>(this TInput a, Func<TInput, TResult> func, Nullable<TResult> defaultValue)
        where TInput : Attribute
        where TResult : struct
    //Have to restrict to struct or you get the error:
    //The type 'R' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'System.Nullable<T>'
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    //In order to constrain to a class without interfering with the overload that has a generic struct constraint
    //we need to add a parameter to the signature that is a reference type restricted to a class
    public class ClassConstraintHack<T> where T : class { }

    //The hack means we have an unused parameter in the signature
    //http://msmvps.com/blogs/jon_skeet/archive/2010/11/02/evil-code-overload-resolution-workaround.aspx
    public static TResult GetValueOrDefault<TInput, TResult>(this TInput a, Func<TInput, TResult> func, TResult defaultValue, ClassConstraintHack<TResult> ignored = default(ClassConstraintHack<TResult>))
        where TInput : Attribute
        where TResult : class
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    //I don't go so far as to use the inheritance trick decribed in the evil code overload resolution blog, 
    //just create some overloads that take nullable types - and I will just keep adding overloads for other nullable type
    public static bool? GetValueOrDefault<TInput>(this TInput a, Func<TInput, bool?> func, bool? defaultValue)
where TInput : Attribute
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    public static int? GetValueOrDefault<TInput>(this TInput a, Func<TInput, int?> func, int? defaultValue)
where TInput : Attribute
    {
        if (a == null)
            return defaultValue;

        return func(a);
    }

    public static T GetAttribute<T>(this PropertyInfo p) where T : Attribute
    {
        if (p == null)
            return null;

        return p.GetCustomAttributes(false).OfType<T>().LastOrDefault();
    }

    public static PropertyInfo GetProperty<T, R>(Expression<Func<T, R>> expression)
    {
        if (expression == null)
            return null;

        MemberExpression memberExpression = expression.Body as MemberExpression;
        if (memberExpression == null)
            return null;

        return memberExpression.Member as PropertyInfo;
    }
}

【讨论】:

  • 非常感谢您的帖子,感谢您与我们分享您的代码。这段代码很棒,但没有关注我的问题:为某些对象实例设置对象属性为只读,并从 UI 和 dbcontext 验证过程中检查此属性。
  • 谢谢。但是,我认为DataAnnotation 不适合对象的某些实例——它们适用于整个类。我认为你需要一种不同的方法。您能向我们展示一些您现有的代码吗?
  • 科林,我的代码,此时,很无聊。我已经覆盖了context.validateEntity,此时,我填写了一个只读属性列表并检查它没有更改,这个属性列表是通过为我的实体类实现的接口公开的。然后 UI 可以读取这个列表。有点脏,我需要一个更优雅的解决方案。
【解决方案2】:

.NET 允许您通过实现 System.ComponentModel.ICustomTypeDescriptor 来动态更改 Class 的结构。大多数序列化程序都支持此接口。

// Sample Serialization

foreach(PropertyDescriptor p in TypeDescriptor.GetProperties(obj)){
    string name = p.PropertyName;
    object value = p.GetValue(obj);
}

TypeDescriptor 内部使用了 Reflection,但该实现允许我们轻松覆盖反射属性。

这里是实现的三个步骤,

// Implement System.ComponentModel.ICustomTypeDescriptor Interface on
// your Entity

public class MyEntity: System.ComponentModel.ICustomTypeDescriptor
{
     ....
     // most methods needs only call to default implementation as shown below

    System.ComponentModel.AttributeCollection      
    System.ComponentModel.ICustomTypeDescriptor.GetAttributes()
    {
        return TypeDescriptor.GetAttributes(this, true);
    }

    string System.ComponentModel.ICustomTypeDescriptor.GetClassName()
    {
        return TypeDescriptor.GetClassName(this, true);
    }

    string System.ComponentModel.ICustomTypeDescriptor.GetComponentName()
    {
        return TypeDescriptor.GetComponentName(this, true);
    }

    System.ComponentModel.TypeConverter System.ComponentModel.ICustomTypeDescriptor.GetConverter()
    {
        return TypeDescriptor.GetConverter(this, true);
    }

    System.ComponentModel.EventDescriptor System.ComponentModel.ICustomTypeDescriptor.GetDefaultEvent()
    {
        return TypeDescriptor.GetDefaultEvent(this, true);
    }

    System.ComponentModel.PropertyDescriptor System.ComponentModel.ICustomTypeDescriptor.GetDefaultProperty()
    {
        return TypeDescriptor.GetDefaultProperty(this, true);
    }

    object System.ComponentModel.ICustomTypeDescriptor.GetEditor(Type editorBaseType)
    {
        return TypeDescriptor.GetEditor(this, editorBaseType, true);
    }

    System.ComponentModel.EventDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
    {
        return TypeDescriptor.GetEvents(this, attributes, true);
    }

    System.ComponentModel.EventDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetEvents()
    {
        return TypeDescriptor.GetEvents(this, true);
    }

    System.ComponentModel.PropertyDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
    {
        return TypeDescriptor.GetProperties(this, attributes, true);
    }

    object System.ComponentModel.ICustomTypeDescriptor.GetPropertyOwner(System.ComponentModel.PropertyDescriptor pd)
    {
        return this;
    }

    // The Only method that needs different implementation is below
    System.ComponentModel.PropertyDescriptorCollection 
    System.ComponentModel.ICustomTypeDescriptor.GetProperties()
    {
       // ... you are supposed to create new instance of 
       // PropertyDescriptorCollection with PropertyDescriptor

       PropertyDescriptorCollection pdc = new PropertyDescriptorCollection();

       foreach(PropertyDescriptor p in TypeDescriptor.GetProperties(this,true)){
            // if readonly..

            AtomPropertyDescriptor ap = new AtomPropertyDescriptor(p, p.Name);
            // or
            AtomPropertyDescriptor ap = new AtomPropertyDescriptor(p, p.Name, 
                true,
                new XmlIgnoreAttribute(),
                new ScriptIgnoreAttribute(),
                new ReadOnlyAttribute());
            pdc.Add(ap);
       }

       return pdc;
    }
}


// And here is the AtomPropertyDescriptorClass

public class AtomPropertyDescriptor : PropertyDescriptor
{
    PropertyDescriptor desc;

    bool? readOnly = null;

    public AtomPropertyDescriptor(PropertyDescriptor pd, string name, 
        bool? readOnly, params Attribute[] attrs) :
        base(name, attrs)
    {
        desc = pd;
        this.readOnly = readOnly;
    }

    public override bool CanResetValue(object component)
    {
        return desc.CanResetValue(component);
    }

    public override Type ComponentType
    {
        get
        {
            return desc.ComponentType;
        }
    }

    public override object GetValue(object component)
    {
        return desc.GetValue(component);
    }

    public override bool IsReadOnly
    {
        get
        {
            if (readOnly.HasValue)
                return readOnly.Value;
            return desc.IsReadOnly;
        }
    }

    public override Type PropertyType
    {
        get { return desc.PropertyType; }
    }

    public override void ResetValue(object component)
    {
        desc.ResetValue(component);
    }

    public override void SetValue(object component, object value)
    {
        desc.SetValue(component, value);
    }

    public override bool ShouldSerializeValue(object component)
    {
        return desc.ShouldSerializeValue(component);
    }
}

【讨论】:

    【解决方案3】:

    我认为您正在寻找的是这样的自定义注释属性:

    <DisableEditAttribute(this.IsProjectStarted)>
    Public Property NomInvertebrat As String
    
    public override bool IsValid(bool value)
    {
        bool result = true;
        // Add validation logic here.
        if(value)
        {
             //Compare Current Value Against DB Value.
        }
        return result;
    }
    

    参见 MSDN:http://msdn.microsoft.com/en-us/library/cc668224(v=vs.98).aspx

    【讨论】:

      猜你喜欢
      • 2018-07-18
      • 1970-01-01
      • 2016-02-02
      • 2014-04-12
      • 2011-04-24
      • 1970-01-01
      • 2013-02-26
      • 2013-01-13
      • 1970-01-01
      相关资源
      最近更新 更多