Search code examples
c#encryptionnhibernatefluent-nhibernateorchardcms

nHibernate invalid cast with custom PrimitiveType


I am trying to figure out why I am getting an invalid cast exception with nHibernate with the following code:

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 that works:


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

POCO that fails:


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

This is all automapped with Fluent.

Both the Guid type and string type are nvarchar(500) in the SQL database.

As mentioned, the first POCO works fine and encrypts/decrypts as expected, but the second POCO fails, and this is what I see in my logs:

NHibernate.Tuple.Entity.PocoEntityTuplizer.SetPropertyValuesWithOptimizer(Object entity, Object[] values) {"Invalid Cast (check your mapping for property type mismatches); setter of otherclass"}

Note that the second POCO object works fine with nHib if I remove the EncryptedDbString attibute, i.e. it has no problems saving the Guid to a nvarchar.

Obviously the issue here is that it's a Guid as the string case works, but I do want it kept as a Guid not a string in the code, and I can't see the point of failure here.

Seems like I'm missing something small. I guess I'm missing something with the generics, but I've only found code snippets out there rather than a full example like this.

EDIT:

ok, so i figured out it i think it was because the

Get(IDataReader rs, int index) 

was not returning a Guid object.

so I guess you can serialize/deserialize in the EncryptedStringType Get/Set methods, e.g. in the Get() you could change to:

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

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

but that seems horrible, especially if you have existing data to migrate.

i don't want to store stuff as binary either, as the team want to be able to check/test/audit manually via SQL which columns are encrypted (which is obvious with text, but not binary).

a string backing field in my POCO that converts the Guid to a string and back again via simple get/set methods might be the best option, but I have no idea how to do that with automapping across the solution or how messy it is?


Solution

  • Having slept, I think i've been thinking about this the wrong way.

    I've now realised that my reticence to store json in the database was driven by the fact that I am storing string-biased objects - i.e. things that naturally convert to text fields, as opposed to full objects. myGuid.ToString() gives you a guid string, myDateTime.ToString() gives you a datetime string etc.

    So given that object serialisation per se isn't needed in my case, but rather just conversion to a string, Andrew's suggestion seems like a good solution.

    Updated code:

    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;
    }
    

    An improvement would be for the EncryptedStringConvention to have the Accept() method added to pre-check that all the types marked with the EncryptedDbString attribute were convertible. Possibly we could use Convert() and type is IConvertible instead, but I'll leave it as, enough time spent!