nHibernate使用自定义PrimitiveType进行无效转换

时间:2016-12-12 17:56:10

标签: c# encryption nhibernate fluent-nhibernate orchardcms

我试图找出为什么我使用以下代码使用nHibernate获取无效的强制转换异常:

AutoMap.Source(new TypeSource(recordDescriptors))
    .Conventions.Add(new EncryptedStringConvention());

[AttributeUsage(AttributeTargets.Property)]
public class EncryptedDbString : Attribute { }

public class EncryptedStringConvention : IPropertyConvention {
    public void Apply(IPropertyInstance instance) {
        if (!instance.Property.MemberInfo.IsDefined(typeof(EncryptedDbString), false))
            return;

        var propertyType = instance.Property.PropertyType;
        var generic = typeof(EncryptedStringType<>);
        var specific = generic.MakeGenericType(propertyType);
        instance.CustomType(specific);
    }
}

[Serializable]
public class EncryptedStringType<T> : PrimitiveType
{
    const int MaxStringLen = 1000000000;
    public EncryptedStringType() : this(new StringSqlType(MaxStringLen)) { }
    public EncryptedStringType(SqlType sqlType) : base(sqlType) { }

    public override string Name {
        get { return typeof(T).Name; }
    }

    public override Type ReturnedClass {
        get { return typeof(T); }
    }

    public override Type PrimitiveClass {
        get { return typeof(T); }
    }

    public override object DefaultValue {
        get { return default(T); }
    }

    public override object Get(IDataReader rs, string name) {
        return Get(rs, rs.GetOrdinal(name));
    }

    public override void Set(IDbCommand cmd, object value, int index) {
        if (cmd == null) throw new ArgumentNullException("cmd");
        if (value == null) {
            ((IDataParameter)cmd.Parameters[index]).Value = null;
        }
        else {
            ((IDataParameter)cmd.Parameters[index]).Value = Encryptor.EncryptString((string)value);
        }
    }

    public override object Get(IDataReader rs, int index) {
        if (rs == null) throw new ArgumentNullException("rs");
        var encrypted = rs[index] as string;
        if (encrypted == null) return null;
        return Encryptor.DecryptString(encrypted);
    }

    public override object FromStringValue(string xml) {
        // i don't think this method actually gets called for string (i.e. non-binary) storage 
        throw new NotImplementedException();
    }

    public override string ObjectToSQLString(object value, Dialect dialect) {
        // i don't think this method actually gets called for string (i.e. non-binary) storage 
        throw new NotImplementedException();
    }

}

POCO有效:

public class someclass {
   public virtual string id {get;set;}
   [EncryptedDbString]
   public virtual string abc {get;set;}
}

失败的POCO:

public class otherclass {
   public virtual string id {get;set;}
   [EncryptedDbString]
   public virtual Guid def {get;set;}
}

这一切都是使用Fluent自动完成的。

Guid类型和字符串类型在SQL数据库中都是nvarchar(500)。

如前所述,第一个POCO工作正常并按预期加密/解密,但第二个POCO失败,这就是我在日志中看到的:

NHibernate.Tuple.Entity.PocoEntityTuplizer.SetPropertyValuesWithOptimizer(Object entity,Object [] values) {“Invalid Cast(检查映射中的属性类型不匹配); setter of otherclass”}

请注意,如果我删除了EncryptedDbString属性,则第二个POCO对象可以正常使用nHib,即将Guid保存到nvarchar没有问题。

显然这里的问题是它是一个Guid,因为字符串的情况有效,但是我确实希望它保存为Guid而不是代码中的字符串,我看不出在这里失败。

好像我错过了一些小事。我想我遗漏了泛型的东西,但我只找到了代码片段,而不是像这样的完整例子。

修改

好吧,所以我想出来,我认为这是因为

Get(IDataReader rs, int index) 

没有返回Guid对象。

所以我猜你可以在EncryptedStringType Get / Set方法中序列化/反序列化,例如在Get()中你可以改为:

if (typeof(T) == typeof(string))
    return decrypted;

var obj = JsonConvert.DeserializeObject(decrypted);
return obj;

但这似乎很糟糕,特别是如果你有现有的数据需要迁移。

我不希望将内容存储为二进制文件,因为团队希望能够通过SQL手动检查/测试/审核哪些列是加密的(这对文本很明显,但是不是二元)。

我的POCO中的字符串支持字段将Guid转换为字符串并通过简单的get / set方法再次返回可能是最佳选择,但我不知道如何通过自动化整个解决方案或者它是多么混乱是

1 个答案:

答案 0 :(得分:1)

睡觉了,我想我一直在考虑这个问题。

我现在意识到我在数据库中存储json的沉默是由于我存储字符串偏向的对象 - 即自然转换为文本字段而不是完整对象的事实。 myGuid.ToString()为你提供了一个guid字符串,myDateTime.ToString()为你提供了一个日期时间字符串等。

因此,鉴于在我的情况下不需要对象序列化本身,而只是转换为字符串,Andrew的建议似乎是一个很好的解决方案。

更新的代码:

public override void Set(IDbCommand cmd, object value, int index) {

    var prm = ((IDataParameter) cmd.Parameters[index]);
    if (cmd == null) throw new ArgumentNullException("cmd");
    if (value == null) {
        prm.Value = null;
        return;
    }

    string str;
    try {
        // guid becomes a simple guid string, datetime becomes a simple     
        // datetime string etc. (ymmv per type)
        // note that it will use the currentculture by 
        // default - which is what we want for a datetime anyway
        str = TypeDescriptor.GetConverter(typeof(T)).ConvertToString(value);
    }
    catch (NotSupportedException) {
        throw new NotSupportedException("Unconvertible type " + typeof(T) + " with EncryptedDbString attribute");
    }

    prm.Value = Encryptor.EncryptString(str);

}

public override object Get(IDataReader rs, int index) {

    if (rs == null) throw new ArgumentNullException("rs");
    var encrypted = rs[index] as string;
    if (encrypted == null) return null;

    var decrypted = Encryptor.DecryptString(encrypted);

    object obj;
    try {
        obj = (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(decrypted);
    }
    catch (NotSupportedException) {
        throw new NotSupportedException("Unconvertible type " + typeof(T) + " with EncryptedDbString attribute");
    }
    catch (FormatException) {
        // consideration - this will log the unencrypted text
        throw new FormatException(string.Format("Cannot convert string {0} to type {1}", decrypted, typeof(T)));
    }

    return obj;
}

EncryptedStringConvention的改进是添加Accept()方法以预先检查标记有EncryptedDbString属性的所有类型是否可转换。可能我们可以使用Convert()而类型是IConvertible,但是我会留下它,花费足够的时间!