Table of Contents

Ranges and Multiranges

PostgreSQL has the unique feature of supporting range data types. Ranges represent a range of numbers, dates or other data types, and allow you to easily query ranges which contain a value, perform set operations (e.g. query ranges which contain other ranges), and other similar operations. The range operations supported by PostgreSQL are listed in this page. The Npgsql EF Core provider allows you to seamlessly map PostgreSQL ranges, and even perform operations on them that get translated to SQL for server evaluation.

In addition, PostgreSQL 14 introduced multiranges, which are basically sorted arrays of non-overlapping ranges with set-theoretic operations defined over them. Most range operators also work on multiranges, and they have a few functions of their own. Multirange support in the EF Core provider was introduced in version 6.0.0.

Ranges

Npgsql maps PostgreSQL ranges to the generic CLR type NpgsqlRange<T>:

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public NpgsqlRange<DateTime> Duration { get; set; }
}

This will create a column of type daterange in your database. You can similarly have properties of type NpgsqlRange<int>, NpgsqlRange<long>, etc.

User-defined ranges

PostgreSQL comes with 6 built-in ranges: int4range, int8range, numrange, tsrange, tstzrange, daterange; these can be used simply by adding the appropriate NpgsqlRange<T> property in your entities as shown above. You can also define your own range types over arbitrary types, and use those in EF Core as well.

To make the EF Core type mapper aware of your user-defined range, call the MapRange() method in your context's OnConfiguring() method as follows:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseNpgsql(
        "<connection_string>",
        options => options.MapRange<float>("floatrange"));

This allows you to have properties of type NpgsqlRange<float>, which will be mapped to PostgreSQL floatrange.

The above does not create the floatrange type for you. In order to do that, include the following in your context's OnModelCreating():

protected override void OnModelCreating(ModelBuilder modelBuilder)
    => modelBuilder.HasPostgresRange("floatrange", "real");

This will cause the appropriate CREATE TYPE ... AS RANGE statement to be generated in your migrations, ensuring that your range is created and ready for use. Note that HasPostgresRange() supports additional parameters as supported by PostgreSQL CREATE TYPE.

Multiranges

Note

This feature was introduced in version 6.0

Npgsql maps arrays or lists of NpgsqlRange<T> to PostgreSQL multiranges:

public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public NpgsqlRange<DateTime>[] OpeningTimes { get; set; }
}

Operation translation

Ranges can be queried via extensions methods on NpgsqlRange:

var events = context.Events.Where(p => p.Duration.Contains(someDate));

This will translate to an SQL operation using the PostgreSQL @> operator, evaluating at the server and saving you from transferring the entire Events table to the client. Note that you can (and probably should) create indexes to make this operation more efficient, see the PostgreSQL docs for more info.

The following table lists the range operations that currently get translated. Most operations work on both ranges and multiranges (starting with version 6.0.0); the multirange version is omitted for brevity.

If you run into a missing operation, please open an issue.

.NET SQL
range.LowerBound lower(range)
range.UpperBound upper(range)
range.LowerBoundIsInclusive lower_inc(range)
range.UpperBoundIsInclusive upper_inc(range)
range.LowerBoundIsInfinite lower_inf(range)
range.UpperBoundIsInfinite upper_inf(range)
range.IsEmpty isempty(range)
multirange.Any() NOT is_empty(multirange)
range.Contains(i) range @> i
range1.Contains(range2) range @> range2
range1.ContainedBy(range2) range1 <@ range2
range1.Overlaps(range2) range1 && range2
range1.IsStrictlyLeftOf(range2) range1 << range2
range1.IsStrictlyRightOf(range2) range1 >> range2
range1.DoesNotExtendLeftOf(range2) range1 &> range2
range1.DoesNotExtendRightOf(range2) range1 <& range2
range1.IsAdjacentTo(range2) range1 -|- range2
range1.Union(range2) range1 + range2
range1.Intersect(range2) range1 * range2
range1.Except(range2) range1 - range2
range1.Merge(range2) range_merge(range1, range2)
multirange.Merge() range_merge(multirange)
ranges.RangeAgg() range_agg(ranges)
ranges.RangeIntersectAgg() range_intersect_agg(ranges)
multiranges.RangeIntersectAgg() range_intersect_agg(multiranges) Added in 7.0, See Aggregate functions.