This example illustrates how to build a web gis application using PostGIS, ASP .NET Core and Open Layers
This example illustrates :
architecture of these applications
the components that are used to build a web GIS application
Interaction between these components
The application should list the municipalities and when the user clicks on the name of a municipality, a filled polygon representing the municipality map should be displayed over the map.
For developing this application, a stack of tools should be used. The diagram below explains the used tools and their interaction
Let's explore the components one by one.
Data is stored in a table called municipalities in a Postgree Database.
In order to run the example, make sure that you installed Posgree and the PostGIS extension. Make sure you installed PG Admin as graphical user interface to manage the installed Postgree server.
Create a database but do not create a table at this time.
Since we will use an ASP .Net Core project with a code first approach, the Entity Framework will create the table automatically from the data model.
Once the table is created you should import the data of the table using COPY command.
An example of the command is below:
COPY municipalities (id, municipality, geometry)
FROM 'c:\\ai\\municipalities.csv’
WITH (FORMAT csv, HEADER true, DELIMITER ',');
The muncipalities.csv file is attached in this section.
Update the path according to where you downloaded the file.
Execute the COPY command only after the table is created from the model in Entity Framework. NOT NOW.
In Visual Studio create an ASP .NET Core Web App Project of type Model-View-Controller, programming language C#, as in the illustrative picture
Using NuGet package manager, find and install the following packages:
Microsoft.EntityFramework.Core
Microsoft.EntityFramework.Core.Design
Microsoft.EntityFramework.Core.Tools
Npgsql.EntityFrameworkCore.PostgreSQL
Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite
NetTopologySuite.IO.Json
Add a connection string at appsettings.json file.
A version of appsettings.json file is brought below.
Make sure to replace your reference to your server and credentials in the connection string
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=GISCourse2025;Username=postgres;Password=postgres"
}
Create a folder Data in your project.
Within that folder create a file named ApplicationDBContext.cs
Below is an example of the ApplicationDBContext.cs file:
using Microsoft.EntityFrameworkCore;
using NetTopologySuite;
using NetTopologySuite.Geometries;
namespace GISCourse2025.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=GISCourse2025;Username=postgres;Password=postgres",
o => o.UseNetTopologySuite());
}
public DbSet<GISCourse2025.Models.municipalities> municipalities { get; set; }
}
}
When you create this file make sure you update the credentials to access your server and the database. If the database ndoes not exist the entity framework will create it for you.
The row:
public DbSet<GISCourse2025.Models.municipalities> municipalities { get; set
declares a data model for muncipalities. At first this might be underlined with red telling that is is an error. It is normal because we haven't yet created the municipalities model.
Make sure that your replace the GISCourse2025 with your project namespace. Usualy this is the same name as the project. If you are not sure see other classes that are created automaticaly and find the correct name for your namespace.
Open progam.cs file and add the following line before the command
var app = builder.Build();
Code to add, that registers the data context in your project
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
Under folder Models create a file named municipalities.cs. Make sure that the name is writen correctly and with lowercase letters.
Add the following code to the file:
using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries;
namespace GISCourse2025.Models
{
public class municipalities
{
public int id { get; set; }
public string name { get; set; }
public Geometry geometry { get; set; }
}
}
Again, make sure you replace GISCourse2025 with the namespace of your project
Now that we have setup up the project, prepared the data model, we will apply the data model to the database.
Using Package Manager Console in Visual Studio run the following commands:
add-migration Initial Create
Have a look at folder Migrations, you will find a file with the name starting with numbers (showing time) and ending with InitialCreate. Open the file but do not modify anything on it. Just read the content and intuitively you should understand that the command generated the c# code to create the municipalities table as it is described in Models/municipalities.cs class.
Now execute this migration by executing command:
udpate-database
This would create the database and municipalities table in Postgres.
Open PG Admin and verify that table municipalities is created as per the definition in the municipalities.cs class.
In PG Admin create a new query editor and insert the following command:
COPY municipalities (id, municipality, geometry)
FROM 'c:\\ai\\municipalities.csv’
WITH (FORMAT csv, HEADER true, DELIMITER ',');
Replace in the code the correct path where you placed municipalities.csv file and run the command.
The data should be imported in table municipalities.
Verify it by executing a SELECT query against municipalities table
☕ If you made it this point grap a coffe and have a break because now we are about the write our application
Municipalities control is responsible for taking end user request and serving Municipalities.cshtml view.
MunicipalitiesController.cs
using GISCourse2025.Data;
using GISCourse2025.Models;
using Microsoft.AspNetCore.Mvc;
using NetTopologySuite.Geometries;
namespace GISCourse2025.Controllers
{
public class MunicipalitiesController : Controller
{
ApplicationDbContext context;
public MunicipalitiesController(ApplicationDbContext context)
{
this.context = context;
}
public IActionResult Index()
{
List<municipalities> municipalities1 = context.municipalities
.Where(k => k.id > 0)
.ToList();
return View("Municipalities", municipalities1);
}
}
}
}
The controller constructor injects the DBContext.
this.context = context;
While the default method Index, gets the records of the table municipalities and returns a Municipalities View, passing as well the list of the municipalities.
return View("Municipalities", municipalities1);
Map Wep API publishes a web method that gets as parameter the id of the municipality and returns its geometric shape.
api/MapController.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using NetTopologySuite.Geometries;
using System.Text.Json;
using GISCourse2025.Data;
using GISCourse2025.Models;
using NetTopologySuite.IO;
namespace GISCourse2025.API
{
[Route("api/[controller]")]
[ApiController]
public class MapController : ControllerBase
{
private readonly ApplicationDbContext _context;
public MapController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet("{id}")]
public IActionResult Index(int id)
{
if (id <1 )
{
id = 23610;
}
var multipolygons = _context.municipalities
.Select(p => new
{
p.id,
p.name,
p.geometry
})
.Where(p => p.id == id)
.ToList();
var multipoligon = multipolygons.FirstOrDefault();
var wktWriter = new WKTWriter();
string wkt = wktWriter.Write(multipoligon.Geometry);
return Ok(wkt);
}
}
The polygon of the municipality is returned in WKT : Well Knwon Text format.
This format is a knwon format for OpenLayers library.
There are as well other formats to pass geometry data to OpenLayers , such a GeoJson etc.
Municipalities view, creates the user interface for our map application.
It lists the municipalities at left and uses OpenLayers to create a map layer using OSM as a base.
Complete code of Municipalities.cshtml
@model IEnumerable<GISCourse2025.Models.municipalities>
@{
ViewData["Title"] = "Municipalities";
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenLayers Polygon</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@latest/ol.css">
<script src="https://cdn.jsdelivr.net/npm/ol@latest/dist/ol.js"></script>
</head>
<body>
<table width="100%">
<tr>
<td width="20%">
<h2 >Municipalities</h2>
<p>List of municipalities</p>
@foreach (municipalities m in Model)
{
<p>
<a href="#" onclick="loadPolygon(@m.id)"> @m.name </a></p>
}
</td>
<td valign="top">
<h2>Municipalities in the Map</h2>
<div id="map" style="width: 100%; height: 1000px;float:right;"></div>
</td>
</tr>
</table>
<script>
var map; //global variable
function InitMap() //function to initialize map with OSM
{
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([19.818698, 41.327546]), // Adjust center based on polygon
zoom: 8
})
});
}
async function loadPolygon(id)
{
// Fetch WKT from ASP.NET API
const response = await fetch('../../api/map/'+id); // Replace '1' with the actual polygon ID
if (!response.ok)
{
console.error("Failed to load polygon");
return;
}
const wkt = await response.text();
console.log("Polygon WKT:", wkt);
// Convert WKT to OpenLayers Feature
var format = new ol.format.WKT();
var feature = format.readFeature(wkt,
{
dataProjection: 'EPSG:4326', // Matches the database format
featureProjection: 'EPSG:3857' // OpenLayers uses EPSG:3857
}
);
// Define vector layer for the polygon
var vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
features: [feature]
}),
style: new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'blue',
width: 2
}),
fill: new ol.style.Fill({
color: 'rgba(0, 0, 255, 0.3)'
})
})
});
map.addLayer(vectorLayer);
}
// loadPolygon();
InitMap();
</script>
</body>
</html>
Let's explain parts of the code
@model IEnumerable<GISCourse2025.Models.municipalities>
Tells the view what datatype is accepting from the controller. Replace GISCourse2025 with your projetc's namespace.
<td width="20%">
<h2 >Municipalities</h2>
<p>List of municipalities</p>
@foreach (municipalities m in Model)
{
<p>
<a href="#" onclick="loadPolygon(@m.id)"> @m.name </a></p>
}
</td>
Creates a list of municipalities with a link that calls a javascript function loadPolygon pssing to it the id of the municipality
<h2>Municipalities in the Map</h2>
<div id="map" style="width: 100%; height: 1000px;float:right;"></div>
Creates an element on the page id=map that will be used bu the Open Layers libraries to display the map
var map; //global variable
Defines a javascript variable outside of any function.
function InitMap() //function to initialize map with OSM
{
map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([19.818698, 41.327546]), // Adjust center based on polygon
zoom: 8
})
});
}
This function Initializes the map using OSM as a base source, then creates a view of the map, centered on a coordinate. The view defines the zoom level as well.
LoadPolugon(id) function :
calls web api and gets the shape of the choosen municipality.
creates a vector layer,
transforms the projection of the layer to the projecteion used by Open Layers
adds the layer to the intitialized map
async function loadPolygon(id)
{
// Fetch WKT from ASP.NET API
const response = await fetch('../../api/map/'+id); // Replace '1' with the actual polygon ID
if (!response.ok)
{
console.error("Failed to load polygon");
return;
}
const wkt = await response.text();
console.log("Polygon WKT:", wkt);
// Convert WKT to OpenLayers Feature
var format = new ol.format.WKT();
var feature = format.readFeature(wkt,
{
dataProjection: 'EPSG:4326', // Matches the database format
featureProjection: 'EPSG:3857' // OpenLayers uses EPSG:3857
}
);
// Define vector layer for the polygon
var vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
features: [feature]
}),
style: new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'blue',
width: 2
}),
fill: new ol.style.Fill({
color: 'rgba(0, 0, 255, 0.3)'
})
})
});
map.addLayer(vectorLayer);
}