具有自定义 PrimitiveType 的 nHibernate 无效转换

nHibernate invalid cast with custom PrimitiveType

我想弄清楚为什么我使用以下代码在 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类型和string类型在SQL数据库中都是nvarchar(500)

如前所述,第一个 POCO 工作正常并且 encrypts/decrypts 正如预期的那样,但是第二个 POCO 失败了,这是我在日志中看到的:

NHibernate.Tuple.Entity.PocoEntityTuplizer.SetPropertyValuesWithOptimizer(对象实体,对象[]值) {"Invalid Cast (check your mapping for property type mismatches); setter of otherclass"}

请注意,如果我删除 EncryptedDbString 属性,第二个 POCO 对象可以与 nHib 一起正常工作,即将 Guid 保存为 nvarchar 没有问题。

显然这里的问题是它是一个 Guid,因为字符串大小写有效,但是我确实希望它在代码中保持为 Guid 而不是字符串,我可以'看不到这里的失败点。

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

编辑:

好的,所以我想通了 我认为这是因为

Get(IDataReader rs, int index) 

未返回 Guid 对象。

所以我猜你可以在 EncryptedStringType Get/Set 方法中 serialize/deserialize,例如在 Get() 中,您可以更改为:

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

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

但这看起来很糟糕,尤其是当您有现有数据要迁移时。

我也不想将内容存储为二进制文件,因为团队希望能够通过 SQL 手动 check/test/audit 哪些列是加密(对于文本很明显,但不是二进制)。

我的 POCO 中的一个字符串支持字段通过简单的 get/set 方法将 Guid 转换为字符串并再次返回可能是最好的选择,但我不知道如何通过跨解决方案的自动映射来做到这一点或者有多乱?

睡了一觉,我想我一直在以错误的方式思考这个问题。

我现在意识到我不愿在数据库中存储 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,但我会保留它,足够的时间!