惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
量子位
腾讯CDC
月光博客
月光博客
博客园 - 【当耐特】
博客园 - 聂微东
罗磊的独立博客
aimingoo的专栏
aimingoo的专栏
D
DataBreaches.Net
Apple Machine Learning Research
Apple Machine Learning Research
F
Fortinet All Blogs
博客园 - Franky
爱范儿
爱范儿
L
LangChain Blog
云风的 BLOG
云风的 BLOG
TaoSecurity Blog
TaoSecurity Blog
N
News and Events Feed by Topic
Security Archives - TechRepublic
Security Archives - TechRepublic
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
S
Schneier on Security
H
Help Net Security
H
Heimdal Security Blog
The GitHub Blog
The GitHub Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
N
Netflix TechBlog - Medium
Microsoft Azure Blog
Microsoft Azure Blog
Cyberwarzone
Cyberwarzone
Cloudbric
Cloudbric
Recorded Future
Recorded Future
Hacker News: Ask HN
Hacker News: Ask HN
S
Security @ Cisco Blogs
Project Zero
Project Zero
AWS News Blog
AWS News Blog
Spread Privacy
Spread Privacy
MyScale Blog
MyScale Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
S
Securelist
Recent Announcements
Recent Announcements
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
C
CERT Recently Published Vulnerability Notes
M
MIT News - Artificial intelligence
IT之家
IT之家
Google Online Security Blog
Google Online Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Using PostGIS with Azure Database for PostgreSQL
Sam Vanhoutt · 2026-05-02 · via DEV Community

With libelo we are building a platform for discovering parks and nature highlights. Almost all entities in our platform have a location attached: parks have boundaries, highlights like waterfalls or viewpoints have a precise coordinate, and users browse the app with their current position in hand. That means almost every interesting query the backend runs is spatial in some way. Which parks are nearby? Which park contains this highlight? What is the closest trail to where I am standing right now?

You could answer those questions by pulling coordinates out of the database and doing the math in application code. For a small dataset that works. The moment your dataset grows (people are adding new highlights every daily) and you want to filter and sort spatially at query time, you need the database to do that work. PostGIS is the extension that gives PostgreSQL exactly that capability, and it is available out of the box on Azure Database for PostgreSQL Flexible Server.

This post walks through how to enable it, how to wire it into a .NET application using Entity Framework Core and NetTopologySuite, and I'll use the auto-linking of highlights to parks as a concrete example.

What PostGIS gives you

PostGIS adds native geometry and geography column types to PostgreSQL, along with several hundred spatial functions that operate on them. The ones that matter most for an application like Libelo are:

ST_DWithin

Returns true if two geometries are within a given distance of each other. This is the workhorse for "find everything within X kilometers of this point."

SELECT name FROM parks
WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(4.9041, 52.3676), 4326), 10000);
-- returns all parks within 10 km of Amsterdam city centre

Enter fullscreen mode Exit fullscreen mode

ST_Distance

Calculates the exact distance between two geometries. You use this to order results by proximity.

SELECT name, ST_Distance(location, ST_SetSRID(ST_MakePoint(4.9041, 52.3676), 4326)) AS distance_m
FROM parks
ORDER BY distance_m;
-- returns all parks sorted by distance from a point, closest first

Enter fullscreen mode Exit fullscreen mode

ST_Contains

Returns true if one geometry completely contains another. This is how you answer "is this highlight inside the boundaries of a given park?"

SELECT name FROM parks
WHERE ST_Contains(location::geometry, ST_SetSRID(ST_MakePoint(4.9180, 52.3542), 4326)::geometry);
-- returns the park whose boundary polygon contains the given coordinate

Enter fullscreen mode Exit fullscreen mode

GiST indexes

A special index type that understands spatial data. Without a GiST index, every spatial query is a full table scan. Using such an index will make proximity queries on a table with millions of rows complete in milliseconds.

PostGIS also distinguishes between geometry and geography column types. Geometry works in a flat 2D plane and uses whatever units your coordinate system defines. Geography works on the surface of the Earth and always uses meters for distance calculations. For a platform such as libelo, where users are scattered across the globe, geography is the right choice because it stays accurate at large distances without requiring a projected coordinate system.

Every geometry or geography value in PostGIS is tagged with an SRID: a Spatial Reference ID that identifies which coordinate system the coordinates belong to. SRID 4326 refers to WGS 84, the coordinate system that GPS uses. It represents positions on the Earth's surface as latitude and longitude in decimal degrees, with the origin at the Greenwich meridian and the equator. When you store a point as (4.9041, 52.3676) with SRID 4326, PostGIS knows those numbers mean longitude 4.9041 and latitude 52.3676, and it can do correct Earth-surface calculations with them. Without the SRID tag, coordinates are just numbers and spatial functions have no frame of reference to work with. You will see 4326 appear in column type declarations, geometry constructors, and index definitions throughout this post. It is always doing the same job: telling PostGIS that the data is in the global GPS coordinate system.

Enabling PostGIS on Azure Database for PostgreSQL

On Azure Database for PostgreSQL Flexible Server, PostGIS is a pre-installed extension that you activate rather than install. Before you can use it, you need to allowlist it in the server configuration.

In the Azure Portal, navigate to your PostgreSQL Flexible Server, open the Server parameters blade, and search for azure.extensions. Add POSTGIS to the allowed list and save. This tells Azure that the extension is permitted to be loaded.

Once it is allowlisted, you enable it in your database with a single SQL statement:

CREATE EXTENSION IF NOT EXISTS postgis;

Enter fullscreen mode Exit fullscreen mode

If you are using Flyway for migrations (as we do on Libelo), put this in your first migration file and it will run once on initial database setup:

-- V001__Enable_extensions.sql
CREATE EXTENSION IF NOT EXISTS postgis;

Enter fullscreen mode Exit fullscreen mode

In case of using Entity Framework (which we do for now), you also declare the extension in your EF Core model so that tooling is aware of it:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasPostgresExtension("postgis");
    // ...
}

Enter fullscreen mode Exit fullscreen mode

Setting up the .NET packages

NetTopologySuite is the .NET library that represents spatial types like Point, Polygon, and MultiPolygon. The Npgsql EF Core provider has a plugin that bridges between NetTopologySuite and PostGIS, translating LINQ expressions like IsWithinDistance into the correct PostGIS SQL functions.

Add the following packages to your infrastructure project:

<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="10.0.0" />

Enter fullscreen mode Exit fullscreen mode

Then enable the plugin when configuring the database connection:

options.UseNpgsql(connectionString, npgsql =>
    npgsql.UseNetTopologySuite());

Enter fullscreen mode Exit fullscreen mode

That one call is what makes the translation layer work. Without it, EF Core has no idea that Point and MultiPolygon should map to PostGIS geography columns, and spatial LINQ methods will not translate to SQL.

Defining geometry columns

On Libelo, parks have boundaries and highlights have positions. These map to different geometry types.

A highlight is a single point: a specific waterfall, a peak, a cave entrance. The data entity stores it as a Point:

[Column("location", TypeName = "geography(Point, 4326)")]
public Point Location { get; set; } = null!;

Enter fullscreen mode Exit fullscreen mode

A park is a boundary, and parks are not always simple single polygons. A national park can have non-contiguous sections separated by private land, or a coastal reserve can include several islands. See the screenshot of our app.
Using MultiPolygon handles that without needing special cases:

public MultiPolygon Location { get; set; } =
    new MultiPolygon(Array.Empty<Polygon>()) { SRID = 4326 };

Enter fullscreen mode Exit fullscreen mode

Multipolygon parks

The entity configurations register the column types and create GiST indexes:

// Park configuration
builder.Property(p => p.Location)
    .HasColumnType("geography(MultiPolygon, 4326)");

builder.HasIndex(p => p.Location)
    .HasMethod("gist");

// Highlight configuration
builder.Property(e => e.Location)
    .HasColumnType("geography(Point, 4326)")
    .IsRequired();

builder.HasIndex(e => e.Location)
    .HasMethod("gist");

Enter fullscreen mode Exit fullscreen mode

The GiST index is not optional. PostGIS spatial queries can only use index-accelerated lookup when a GiST index exists on the column.

Auto-linking highlights to parks

When a user submits a new highlight through the app, they provide a coordinate. They do not define if the highlight is linked with a park. (we don't want users to do our data management, right?) The backend automatically determines which park that coordinate falls inside, and links the highlight to it. This is one of the places where PostGIS earns its place.

The containment check runs a PostGIS query that asks: does any park's boundary polygon contain this point?

public async Task<Park?> GetContainingPointAsync(
    Point location,
    CancellationToken cancellationToken = default)
{
    var entity = await context.Parks
        .AsNoTracking()
        .Where(p => p.DeletedAt == null)
        .Where(p => p.Location.IsWithinDistance(location, 0))
        .FirstOrDefaultAsync(cancellationToken);

    return entity == null ? null : MapToDomain(entity);
}

Enter fullscreen mode Exit fullscreen mode

The IsWithinDistance(location, 0) call translates to ST_DWithin(location, @point, 0) in SQL. Using a distance of zero is the geography-safe way to test containment. PostGIS has a ST_Contains function, but it only works on the geometry type, not geography. Because our columns are geography for accurate Earth-surface distance calculations, ST_DWithin(..., 0) is the correct idiom: a point is "within zero meters" of a polygon only when it lies inside it.

In the service layer, this containment check plugs into the highlight creation flow. When a highlight is created without an explicit park ID, the service resolves the park automatically:

private async Task<Guid?> ResolveParkIdAsync(
    Guid? parkId,
    Location location,
    CancellationToken cancellationToken)
{
    if (parkId.HasValue)
        return parkId;

    var containingPark = await parkService.FindParkContainingLocationAsync(
        location.Latitude, location.Longitude, cancellationToken);

    if (containingPark == null)
        return null;

    logger.LogInformation(
        "Auto-linked highlight at ({Lat}, {Lon}) to park {ParkId} ({ParkName})",
        location.Latitude, location.Longitude,
        containingPark.Id, containingPark.Name);

    return containingPark.Id;
}

Enter fullscreen mode Exit fullscreen mode

The result is that users never have to think about park assignment. They drop a pin on a waterfall inside a park, and the link is created in the database automatically. If the coordinate falls outside any park boundary (a highlight on a public trail that is not inside a mapped park, for example), the highlight is simply created without a park association.

Proximity queries

The other core use case is finding things near a location. When the app loads, it sends the user's current coordinates to the backend, and the API returns parks and highlights within a configurable radius, ordered by distance.

The query uses IsWithinDistance for the filter and Distance for the ordering:

var query = context.Parks
    .AsNoTracking()
    .Where(p => p.DeletedAt == null)
    .Where(p => p.Status == published)
    .Where(p => p.Location.IsWithinDistance(queryPoint, radiusMeters))
    .OrderBy(p => p.Location.Distance(queryPoint));

Enter fullscreen mode Exit fullscreen mode

IsWithinDistance translates to ST_DWithin, which uses the GiST index and returns only rows within the specified radius before ordering. Without that filter, ORDER BY ST_Distance would scan the entire table to compute distances. The two-step pattern, filter with ST_DWithin then sort with ST_Distance, is the standard way to write efficient proximity queries in PostGIS.

The query point is constructed from the user's latitude and longitude with an explicit SRID:

var geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
var queryPoint = geometryFactory.CreatePoint(new Coordinate(longitude, latitude));

Enter fullscreen mode Exit fullscreen mode

Note that NetTopologySuite follows the GIS convention of (x, y) which means (longitude, latitude), not (latitude, longitude). This is a common source of bugs when first working with the library.

What the database sees

If you connect to the database directly and inspect what is happening, the EF Core queries translate cleanly into PostGIS SQL. A nearby parks query looks like this:

SELECT p.*
FROM parks p
WHERE p.deleted_at IS NULL
  AND p.status = 'Published'
  AND ST_DWithin(p.location, ST_SetSRID(ST_MakePoint($1, $2), 4326), $3)
ORDER BY ST_Distance(p.location, ST_SetSRID(ST_MakePoint($1, $2), 4326));

Enter fullscreen mode Exit fullscreen mode

And the containment check for auto-linking becomes:

SELECT p.*
FROM parks p
WHERE p.deleted_at IS NULL
  AND ST_DWithin(p.location, ST_SetSRID(ST_MakePoint($1, $2), 4326), 0)
LIMIT 1;

Enter fullscreen mode Exit fullscreen mode

These are exactly the queries you would write by hand. The NetTopologySuite translation layer does not add overhead or generate surprising SQL.

Keeping it simple

The setup has a few moving pieces: enabling the extension on Azure, adding two NuGet packages, one line in the DbContext configuration, and geography column types with GiST indexes in the entity configurations. Beyond that, spatial queries in EF Core look like any other LINQ query. You do not need to drop down to raw SQL or manage spatial types manually.

The main thing to be aware of is the geography versus geometry distinction. Geography columns use spherical calculations and always measure distance in meters, which is what you want for a mapping application. If you use geometry columns instead, you lose that accuracy and distance calculations become coordinate-system-dependent. Stick with geography and SRID 4326 and the math stays correct regardless of where in the world your data is.