Skip to content

Unable to map inet column to (IPAddress, int) tuple #1158

@markron

Description

@markron

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions