Skip to content

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

Closed as not planned
@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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions