I have a database which is accessed by multiple systems. Ours reads a table using Fluent Nhibernate and Automapping.
One of the columns is a string representing an enum. In most installations this is fine, but every now and then another system will persist an invalid value. How can I catch these invalid values and either convert or ignore them?
Example:
Database Table:
ID MyEnum
0 "One"
1 "Two"
2 "Invalid"
MyEnum
{
One,
Two
}
I'm already using IAutoMappingOverride with my model, is there anyway to pass a function to the mapper to do validation/conversion (say I wanted to convert any unknowns to MyEnum.One)?
Or is there a simpler way that I'm missing?
Where I got to was using a combination of #Firo's answer and the answers here and here
class SafeEnumType<T> : ImmutableUserType where T : struct
{
public override object NullSafeGet(IDataReader rs, string[] names, object owner)
{
T value;
if (Enum.TryParse((string)rs[names[0]], out value))
return value;
else
return default(T);
}
public override void NullSafeSet(IDbCommand cmd, object value, int index)
{
NHibernateUtil.String.NullSafeSet(cmd, value.ToString(), index);
}
public override Type ReturnedType
{
get { return typeof(T); }
}
public override SqlType[] SqlTypes
{
get { return new[] { SqlTypeFactory.GetString(100) }; }
}
}
and adding
mapping.Map(x => x.Type).CustomType(typeof(SafeEnumType<EventType>));
to my AutomapOverride.Override() fn
In NHibernate there is IUserType which lets you define custom conversion code. here is a base class of mine which you can use because enums are immutable types
code
class SpecialEnumUserType : ImmutableUserType
{
public override object NullSafeGet(IDataReader rs, string[] names, object owner)
{
TheSpecialEnum value;
if (Enum.TryParse<TheSpecialEnum>((string)rs[names[0]], out value))
return value;
else
return default(TheSpecialEnum);
}
public override void NullSafeSet(IDbCommand cmd, object value, int index)
{
NHibernateUtil.String.NullSafeSet(cmd, value.ToString(), index);
}
public override Type ReturnedType
{
get { return typeof(TheSpecialEnum); }
}
public override SqlType[] SqlTypes
{
get { return new[] { SqlTypeFactory.GetString(50) }; }
}
}
and convention to apply them to every enum property
class EnumHandlingConvention : IPropertyConvention
{
public void Apply(IPropertyInstance instance)
{
if (instance.Type == typeof(TheSpecialEnum))
{
instance.CustomType<SpecialEnumUserType>();
}
}
}
Related
I tried to add services.AddSingleton<ODataPayloadValueConverter, EnumFlagODataPayloadValueConverter>();
Microsoft.AspNetCore.OData 8.02 is being used.
I want to convert enum flags to to an array instead. ElementalFlagType is for testing purpose.
internal class EnumFlagODataPayloadValueConverter : ODataPayloadValueConverter
{
public override object ConvertToPayloadValue(object value, IEdmTypeReference edmTypeReference)
{
if (value is ElementalFlagType flag)
{
var ret = new List<ElementalFlagType>(11);
foreach (var enumValue in Enum.GetValues<ElementalFlagType>())
{
if (flag.HasFlag(enumValue))
{
ret.Add(enumValue);
}
}
return ret.ToArray();
}
return base.ConvertToPayloadValue(value, edmTypeReference);
}
public override object ConvertFromPayloadValue(object value, IEdmTypeReference edmTypeReference)
{
if (edmTypeReference.IsEnum() && value is ElementalFlagType[] flags)
{
ElementalFlagType ret = ElementalFlagType.None;
foreach (var flag in flags)
{
EnumFlag.Set(ref ret, flag);
}
return ret;
}
return base.ConvertFromPayloadValue(value, edmTypeReference);
}
}
The code is not executed at all.
I also tried something like this but I am not sure how to register it, I tried to add it as well to the service collection. Do I need to clear the default one? ODataSerializerProvider
public class CustomODataSerializerProvider : ODataSerializerProvider
{
private readonly EnumFlagODataEntityTypeSerializer _entityTypeSerializer;
public CustomODataSerializerProvider(IServiceProvider rootContainer)
: base(rootContainer)
{
_entityTypeSerializer = new EnumFlagODataEntityTypeSerializer(this);
}
public override IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
{
if (edmType.Definition.TypeKind == EdmTypeKind.Enum)
{
return _entityTypeSerializer;
}
return base.GetEdmTypeSerializer(edmType);
}
}
In short I wanted to create a custom IUserType to represent IPAddress from .NET (as an inet type in postgresql) and to be able to query it with custom functions via HQL and Linq. I have trouble with implementing a custom function for Linq.
What I have thus far:
A) I am able to map it:
public class SomeEntityMapper : ClassMap<SomeEntity> {
public SomeEntityMapper() {
...
Map(x => x.IpAddressField)
.CustomSqlType("inet")
.CustomType<IPAddressUserType>()
...
}
}
IUserType implementation below:
[Serializable]
public class IPAddressUserType : IUserType
{
public new bool Equals(object x, object y)
{
if (x == null && y == null)
return true;
if (x == null || y == null)
return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
if (x == null)
return 0;
return x.GetHashCode();
}
public object NullSafeGet(DbDataReader rs, string[] names, ISessionImplementor session, object owner)
{
if (names.Length == 0)
throw new InvalidOperationException("Expected at least 1 column");
if (rs.IsDBNull(rs.GetOrdinal(names[0])))
return null;
object value = rs[names[0]];
return value;
}
public void NullSafeSet(DbCommand cmd, object value, int index, ISessionImplementor session)
{
NpgsqlParameter parameter = (NpgsqlParameter) cmd.Parameters[index];
parameter.NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Inet;
if (value == null)
{
parameter.Value = DBNull.Value;
}
else
{
parameter.Value = value;
}
}
public object DeepCopy(object value)
{
if (value == null)
return null;
IPAddress copy = IPAddress.Parse(value.ToString());
return copy;
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
if (cached == null)
return null;
if (IPAddress.TryParse((string)cached, out var address))
{
return address;
}
return null;
}
public object Disassemble(object value)
{
if (value == null)
return null;
return value.ToString();
}
public SqlType[] SqlTypes => new SqlType[] { new NpgsqlSqlType(DbType.String, NpgsqlTypes.NpgsqlDbType.Inet), };
public Type ReturnedType => typeof(IPAddress);
public bool IsMutable => false;
}
public class NpgsqlSqlType : SqlType
{
public NpgsqlDbType NpgDbType { get; }
public NpgsqlSqlType(DbType dbType, NpgsqlDbType npgDbType)
: base(dbType)
{
NpgDbType = npgDbType;
}
public NpgsqlSqlType(DbType dbType, NpgsqlDbType npgDbType, int length)
: base(dbType, length)
{
NpgDbType = npgDbType;
}
public NpgsqlSqlType(DbType dbType, NpgsqlDbType npgDbType, byte precision, byte scale)
: base(dbType, precision, scale)
{
NpgDbType = npgDbType;
}
}
}
B) I am able to query it using HQL and a custom function (inet_equals) I implemented.
using(var session = _factory.OpenSession()) {
var q = session.CreateQuery("from SomeEntity as s WHERE inet_equals(s.IpAddressField, :ip)");
q.SetParameter("ip", IPAddress.Parse("4.3.2.1"), NHibernateUtil.Custom(typeof(IPAddressUserType)));
}
Custom Postgresql Dialect extending the HQL functions implementation below:
public class CustomPostgresqlDialect : PostgreSQL83Dialect
{
public CustomPostgresqlDialect()
{
RegisterFunction("inet_equals", new SQLFunctionTemplate(NHibernateUtil.Boolean, "(?1::inet = ?2::inet)"));
}
}
C) I want to be able to query it using LINQ like so:
using(var session = _factory.OpenSession()) {
var q = session.Query<SomeEntity>()
.Where(s => s.IpAddressField.InetEquals(IPAddress.Parse("4.3.2.1")));
}
Custom LINQ Generator for NHibernate provider below:
public static class InetExtensions
{
public static bool InetEquals(this IPAddress value, IPAddress other)
{
throw new NotSupportedException();
}
}
public class InetGenerator : BaseHqlGeneratorForMethod
{
public InetGenerator()
{
SupportedMethods = new[]
{
ReflectHelper.GetMethodDefinition(() => InetExtensions.InetEquals(default(IPAddress), default(IPAddress)))
};
}
public override HqlTreeNode BuildHql(MethodInfo method, System.Linq.Expressions.Expression targetObject, ReadOnlyCollection<System.Linq.Expressions.Expression> arguments,
HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
{
HqlExpression lhs = visitor.Visit(arguments[0]).AsExpression();
HqlExpression rhs = visitor.Visit(arguments[1]).AsExpression();
return treeBuilder.BooleanMethodCall(
"inet_equals",
new[]
{
lhs,
rhs
}
);
}
}
public class ExtendedLinqToHqlGeneratorsRegistry :
DefaultLinqToHqlGeneratorsRegistry
{
public ExtendedLinqToHqlGeneratorsRegistry()
: base()
{
this.Merge(new InetGenerator());
}
}
Unfortunately when using LINQ I get this exception:
HibernateException: Could not determine a type for class: System.Net.IPAddress
Interestingly, this is the exact same error I get if I omit the NHibernateUtil.Custom(typeof(IPAddressUserType)) parameter inside the HQL query.
Which leads me to believe I am on the right track, but I can't figure out what I may be missing exactly. I assume I need to inform NHibernate inside the Generator that this is a custom UserType (just like I did with the HQL query via the NHibernateUtil.Custom(typeof(IPAddressUserType)) parameter.
I found the solution.
To hint NHibernate to use the proper UserType use: MappedAs extension method, like so:
using(var session = _factory.OpenSession()) {
var q = session.Query<SomeEntity>()
.Where(s => s.IpAddressField.InetEquals(
IPAddress.Parse("4.3.2.1").MappedAs(NHibernateUtil.Custom(typeof(IPAddressUserType))
);
}
The year is 2019 and I'm still stuck on version 3.3.2.GA of NHibernate and the MappedAs extension method exists only on version 4.x onwards.
My scenario was the need to pass a large string as a parameter to a supported method in an HqlGeneratorForMethod.
I created the following class to store any large string to use as a parameter of any method throughout the application:
public class StringClob
{
public StringClob()
{
}
public StringClob(string value)
{
Value = value;
}
public virtual string Value { get; protected set; }
}
In order to link the NHibernateUtil.StringClob type with the string value I had the idea to create a mapping for my class without informing table mapping just the property (I use FluentNHibernate to map class):
public class StringClobMap : ClassMap<StringClob>
{
public StringClobMap()
{
Id(x => x.Value, "VALUE").CustomType("StringClob").CustomSqlType("VARCHAR(MAX)").Length(int.MaxValue / 2);
}
}
And now hypothetically following the example of #krdx the usage would look like this:
using(var session = _factory.OpenSession())
{
var q = session.Query<SomeEntity>()
.Where(s => s.IpAddressField.InetEquals(new StringClob("<large_string_here>")));
// ...
}
Therefore by passing the StringClob class as a parameter, NHibernate gets the custom type defined in the mapping.
I hope I helped those who still use NHibernate 3.3.2.GA.
Consider the following database table (SQL Server 2005). I'd like to use this in EF (v6, .net 4.5.1) with the Translate function but after searching seems this is not supported.
CREATE TABLE Foo
(
pk INT NOT NULL PRIMARY KEY,
Foo VARCHAR(100)
)
Using by-convention mapping that would create a class Foo with a property Foo which is not supported by C# syntax. I tried using the ColumnAttribute:
public partial class Foo
{
[Key]
public virtual int pk {get;set;}
[Column("Foo")]
public virtual string Name {get;set;}
}
This appears to work, but I'd like to make the initial page request load gobs of data via stored procedure and MARS (and use a generic structure so I can reuse it on other pages), so I called the stored procedure and looped through the result sets, calling ObjectContext.Translate via reflection (similar to the below, but this is abbreviated):
var methTranslate = typeof(ObjectContext).GetMethod("Translate", new[] { typeof(DbDataReader), typeof(string), typeof(MergeOption) });
foreach (var className in classNames)
{
// ...
var translateGenericMethod = methTranslate.MakeGenericMethod(classType);
// ...
reader.NextResult();
var enumerable = (IEnumerable)translateGenericMethod.Invoke(ObjectContext,
new object[] { reader, entitySet.Name, MergeOption.AppendOnly });
}
From multiple things I've read, the ColumnAttribute mapping is not supported. From MSDN:
EF does not take any mapping into account when it creates entities using the Translate method. It will simply match column names in the result set with property names on your classes.
And sure enough, I get and error:
The data reader is incompatible with the specified 'Namespace.Foo'. A member of the type, 'Name', does not have a corresponding column in the data reader with the same name.
The problem is, I do not see any alternative or way to specify/hint at the mapping. I could change the class name but that is less desirable than the property names.
Any workarounds, or any other way to dynamically load data without using Translate?
A bit tricky, but doable.
The idea is to utilize the Translate method by implementing and using a custom DbDataReader that performs the required mapping.
Before doing that, let implement a generic DbDataReader class that does just delegating to the underlying DbDataReader:
abstract class DelegatingDbDataReader : DbDataReader
{
readonly DbDataReader source;
public DelegatingDbDataReader(DbDataReader source)
{
this.source = source;
}
public override object this[string name] { get { return source[name]; } }
public override object this[int ordinal] { get { return source[ordinal]; } }
public override int Depth { get { return source.Depth; } }
public override int FieldCount { get { return source.FieldCount; } }
public override bool HasRows { get { return source.HasRows; } }
public override bool IsClosed { get { return source.IsClosed; } }
public override int RecordsAffected { get { return source.RecordsAffected; } }
public override bool GetBoolean(int ordinal) { return source.GetBoolean(ordinal); }
public override byte GetByte(int ordinal) { return source.GetByte(ordinal); }
public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) { return source.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); }
public override char GetChar(int ordinal) { return source.GetChar(ordinal); }
public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) { return source.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); }
public override string GetDataTypeName(int ordinal) { return source.GetDataTypeName(ordinal); }
public override DateTime GetDateTime(int ordinal) { return source.GetDateTime(ordinal); }
public override decimal GetDecimal(int ordinal) { return source.GetDecimal(ordinal); }
public override double GetDouble(int ordinal) { return source.GetDouble(ordinal); }
public override IEnumerator GetEnumerator() { return source.GetEnumerator(); }
public override Type GetFieldType(int ordinal) { return source.GetFieldType(ordinal); }
public override float GetFloat(int ordinal) { return source.GetFloat(ordinal); }
public override Guid GetGuid(int ordinal) { return source.GetGuid(ordinal); }
public override short GetInt16(int ordinal) { return source.GetInt16(ordinal); }
public override int GetInt32(int ordinal) { return source.GetInt32(ordinal); }
public override long GetInt64(int ordinal) { return source.GetInt64(ordinal); }
public override string GetName(int ordinal) { return source.GetName(ordinal); }
public override int GetOrdinal(string name) { return source.GetOrdinal(name); }
public override string GetString(int ordinal) { return source.GetString(ordinal); }
public override object GetValue(int ordinal) { return source.GetValue(ordinal); }
public override int GetValues(object[] values) { return source.GetValues(values); }
public override bool IsDBNull(int ordinal) { return source.IsDBNull(ordinal); }
public override bool NextResult() { return source.NextResult(); }
public override bool Read() { return source.Read(); }
public override void Close() { source.Close(); }
public override T GetFieldValue<T>(int ordinal) { return source.GetFieldValue<T>(ordinal); }
public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken) { return source.GetFieldValueAsync<T>(ordinal, cancellationToken); }
public override Type GetProviderSpecificFieldType(int ordinal) { return source.GetProviderSpecificFieldType(ordinal); }
public override object GetProviderSpecificValue(int ordinal) { return source.GetProviderSpecificValue(ordinal); }
public override int GetProviderSpecificValues(object[] values) { return source.GetProviderSpecificValues(values); }
public override DataTable GetSchemaTable() { return source.GetSchemaTable(); }
public override Stream GetStream(int ordinal) { return source.GetStream(ordinal); }
public override TextReader GetTextReader(int ordinal) { return source.GetTextReader(ordinal); }
public override Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken) { return source.IsDBNullAsync(ordinal, cancellationToken); }
public override Task<bool> ReadAsync(CancellationToken cancellationToken) { return source.ReadAsync(cancellationToken); }
public override int VisibleFieldCount { get { return source.VisibleFieldCount; } }
}
Nothing fancy - annoyingly overriding all abstract/meaningful virtual members and delegate to the underlying object.
Now the reader that performs name mapping:
class MappingDbDataReader : DelegatingDbDataReader
{
Dictionary<string, string> nameToSourceNameMap;
public MappingDbDataReader(DbDataReader source, Dictionary<string, string> nameToSourceNameMap) : base(source)
{
this.nameToSourceNameMap = nameToSourceNameMap;
}
private string GetSourceName(string name)
{
string sourceName;
return nameToSourceNameMap.TryGetValue(name, out sourceName) ? sourceName : name;
}
public override object this[string name]
{
get { return base[GetSourceName(name)]; }
}
public override string GetName(int ordinal)
{
string sourceName = base.GetName(ordinal);
return nameToSourceNameMap
.Where(item => item.Value.Equals(sourceName, StringComparison.OrdinalIgnoreCase))
.Select(item => item.Key)
.FirstOrDefault() ?? sourceName;
}
public override int GetOrdinal(string name)
{
return base.GetOrdinal(GetSourceName(name));
}
}
Again, nothing fancy. Override a few methods and perform a name to column name and vice versa mapping.
Finally, a helper method that does what you are asking:
public static class EntityUtils
{
public static ObjectResult<T> ReadSingleResult<T>(this DbContext dbContext, DbDataReader dbReader)
where T : class
{
var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
var columnMappings = objectContext.GetPropertyMappings(typeof(T))
.ToDictionary(m => m.Property.Name, m => m.Column.Name);
var mappingReader = new MappingDbDataReader(dbReader, columnMappings);
return objectContext.Translate<T>(mappingReader);
}
static IEnumerable<ScalarPropertyMapping> GetPropertyMappings(this ObjectContext objectContext, Type clrEntityType)
{
var metadata = objectContext.MetadataWorkspace;
// Get the part of the model that contains info about the actual CLR types
var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));
// Get the entity type from the model that maps to the CLR type
var entityType = metadata
.GetItems<EntityType>(DataSpace.OSpace)
.Single(e => objectItemCollection.GetClrType(e) == clrEntityType);
// Get the entity set that uses this entity type
var entitySet = metadata
.GetItems<EntityContainer>(DataSpace.CSpace)
.Single()
.EntitySets
.Single(s => s.ElementType.Name == entityType.Name);
// Find the mapping between conceptual and storage model for this entity set
var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
.Single()
.EntitySetMappings
.Single(s => s.EntitySet == entitySet);
// Find the storage property (column) mappings
var propertyMappings = mapping
.EntityTypeMappings.Single()
.Fragments.Single()
.PropertyMappings
.OfType<ScalarPropertyMapping>();
return propertyMappings;
}
ReadSingleResult is the helper method in question. The GetPropertyMappings method is using part of the code from EF6.1 Get Mapping Between Properties and Columns.
Sample usage similar to the provided example:
var readMethodBase = typeof(EntityUtils).GetMethod("ReadSingleResult", new[] { typeof(DbContext), typeof(DbDataReader) });
foreach (var className in classNames)
{
// ...
var readMethod = readMethodBase.MakeGenericMethod(classType);
var result = ((IEnumerable)readMethod.Invoke(null, new object[] { dbContext, dbReader }))
.Cast<dynamic>()
.ToList();
// ...
dbReader.NextResult();
}
Hope that helps.
I have a mapping like this.
public class MyObjectMap : ClassMap<MyObject> {
public MyObjectMap()
{
Component(_ => _.MyItem, key =>
{
key.Map(x => x.MyItemValue).Column("COL");
/** I want to set this value to a particular enum in this mapper **/
key.Map(x => x.MyItemType).AssignSomeValue(MyEnum.MyValueType)
});
}
}
How do I set the value to some particular item type. It is a component of a particular type.
IUserType can do this
class ConstantValueUserType : IUserType
{
NullSafeGet(IDataReader rd, string[] names, object owner)
{
return 5; // Constant Value
}
public object NullSafeSet(ICommand cmd, object value, int index)
{
// empty, we dont want to write
}
public SqlType[] SqlTypes { get { return new SqlType[0]; } }
}
key.Map(x => x.MyItemType).ReadOnly().Formula(((int)MyEnum.MyValueType).ToString()).CustomType<int>();
I am starting to work with Nhibernate and Oracle for a project.
The database is Oracle 9.2 and I can't change the schema or anything.
I am using NH3.0 and the Oracle.DataAccess.dll ver 2.111.7.20.
So far I've mapped a couple of table and have done some queries. Everything works pretty well.
I've bumped into a problem now and I don't know how to fix it.
The company who's designed the database thought it was a good idea to create all the alphanumeric fields as CHAR instead of VARCHAR or VARCHAR2.
I've mapped all these columns as String and my classes have String fields defined.
Later on I was trying to load an entity by its primary key, defined as CHAR(10) in Oracle.
The key I was trying to load is only 7 characters long
EG: 'CI00252'
Apparently my entity can't be loaded.
Profiling the query with NHProf I can see that my query is fine and if I try to execute it in Oracle Sql-Developer I get the resultset.
I can only manage to make it work if I pad my string like this 'CI00252 '.
Considering that most of the fields defined on the database are CHAR, it is impossible for me to pad everything before executing the query.
What can I do to fix this problem?
PS: I've seen some other people with the same problem here but I couldn't find any appropriate answer.
UPDATE:
I was reading a blog and this guy had a similar problem with another data-type.I've tried to adpapt the code
public SqlType[] SqlTypes
{
get
{
SqlType[] types = new SqlType[1];
types[0] = new SqlType(DbType.StringFixedLength);
return types;
}
}
**
and, apparently, everything is working but ... I don't know why.
using System;
using System.Data;
using NHibernate;
using NHibernate.SqlTypes;
using NHibernate.UserTypes;
namespace ConsoleOracleNhibernate.OracleTypes
{
public class CharUserType : IUserType
{
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
string resultString = (string)NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (resultString != null)
return resultString.Trim();
return null;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
if (value == null)
{
NHibernateUtil.String.NullSafeSet(cmd, null, index);
return;
}
value = ((String)value).Trim();
NHibernateUtil.String.NullSafeSet(cmd, value, index);
}
public object DeepCopy(object value)
{
if (value == null) return null;
return string.Copy((String)value);
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public SqlType[] SqlTypes
{
get
{
SqlType[] types = new SqlType[1];
types[0] = new SqlType(DbType.StringFixedLength);
return types;
}
}
public Type ReturnedType
{
get { return typeof(String); }
}
public bool IsMutable
{
get { return false; }
}
public new bool Equals(object x, object y)
{
if (x == null || y == null) return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
}
}
and my mapping:
<key-property name="CustomerCode" column="ANCOCO" type="ConsoleOracleNhibernate.OracleTypes.CharUserType, ConsoleOracleNhibernate" length="10"></key-property>
Is there anyone here who can try to help me to understand what's happening?
My answer is in the update of the question.