Description
I need to store an IP address along with its netmask in an inet
column.
I would like to map the value of the column to a tuple of type (IPAddress, int)
, as I already do with columns of type cidr
.
I tried to explicitly specify the column type, as in the following example:
class Entity
{
// ...
[Column(TypeName = "inet")]
public (IPAddress, int) Address { get; set; }
// ...
}
However, adding the migration fails with the error The property 'Entity.Address' is of type 'ValueTuple<IPAddress, int>' which is not supported by current database provider. Either change the property CLR type or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
The cause seems to be that a RelationalTypeMapping
for the mapping between inet
and a tuple is not defined.
The only RelationalTypeMapping
that is defined for inet columns and configured in NpgsqlTypeMappingSource
is for the mapping between inet
and IPAddress
.
I currently resolved with a custom type mapping source and an additional type mapper:
public class CustomNpgsqlTypeMappingSource : NpgsqlTypeMappingSource
{
public CustomNpgsqlTypeMappingSource(
TypeMappingSourceDependencies dependencies,
RelationalTypeMappingSourceDependencies relationalDependencies,
ISqlGenerationHelper sqlGenerationHelper,
INpgsqlOptions npgsqlOptions = null)
: base(dependencies, relationalDependencies, sqlGenerationHelper, npgsqlOptions)
{
StoreTypeMappings["inet"] =
new RelationalTypeMapping[] {
new NpgsqlInetWithMaskTypeMapping(),
new NpgsqlInetTypeMapping() };
}
}
// Basically copied from NpgsqlCidrTypeMapping
public class NpgsqlInetWithMaskTypeMapping : NpgsqlTypeMapping
{
public NpgsqlInetWithMaskTypeMapping() : base("inet", typeof((IPAddress, int)), NpgsqlDbType.Inet) {}
protected NpgsqlInetWithMaskTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Inet) {}
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new NpgsqlInetWithMaskTypeMapping(parameters);
protected override string GenerateNonNullSqlLiteral(object value)
{
var cidr = ((IPAddress Address, int Subnet))value;
return $"INET '{cidr.Address}/{cidr.Subnet}'";
}
public override Expression GenerateCodeLiteral(object value)
{
var cidr = ((IPAddress Address, int Subnet))value;
return Expression.New(
Constructor,
Expression.Call(ParseMethod, Expression.Constant(cidr.Address.ToString())),
Expression.Constant(cidr.Subnet));
}
static readonly MethodInfo ParseMethod = typeof(IPAddress).GetMethod("Parse", new[] { typeof(string) });
static readonly ConstructorInfo Constructor =
typeof((IPAddress, int)).GetConstructor(new[] { typeof(IPAddress), typeof(int) });
}
and in Startup
:
services.AddDbContext<MyContext>(c => {
c.UseNpgsql(Configuration.GetConnectionString("Default"));
c.ReplaceService<IRelationalTypeMappingSource, CustomNpgsqlTypeMappingSource>();
});
In this way, a property with type (IPAddress, int)
is correctly mapped to the inet
column type when an attribute [Column(TypeName = "inet")]
is specified.
Can I try to make a PR with this additional type mapper defined and configured in NpgsqlTypeMappingSource
? Or are there some drawbacks that I have not considered?