Saturday, 1 August 2026

Property information in Norway with MAUI - Eiendoms API / Matrikkelen

This article will show how looking up data from Eiendoms API (v1) from Kartverket. The data is retrieved from Matrikkelen, the official registry of properties and land parcels in Norway. The demo will show using a MAUI app and the source code with the demo is available in my Github repository to be cloned from here : https://github.com/toreaurstadboss/MauiMapAppDemo.git

Screenshots

Clicking on a property in the map will show the matrikkelen information for the first hit of the properties around the clicked point.

Sunday, 12 July 2026

Android and Google Maps using .NET Maui

MAUI Maps Demo Notes πŸ—Ί️

A short summary of the map demo, measurement mode, and pin icon adjustments.
The source code can be cloned from this Github repo of mine : πŸ‘‰ MauiMapAppDemo Github repo πŸ’Ύ

This demo shows a .NET MAUI map page with two main ideas: regular cabin pins and a measure mode that lets the user tap two points to draw a red line and calculate distance. The page is driven by a view model, while a map behavior keeps the map logic reusable and keeps the XAML clean. In use is also CommunityToolkit.Mvvm to make MVVM easier to implement. The use of Behaviors to implement view specific behavior / functionality is a common pattern when working with client based logic and MVVM and UI controls.

πŸ”’ The app reads Google Maps and Azure Maps keys from user secrets during local development. The keys are not exposed in the solution or this article.

What the solution does

  • Loads cabin pin data from the view model and inits the location around Trondheim in Norway.
  • Supports a measure mode that tracks start and end taps (by clicking a button to activate this)
  • Draws a polyline (line segment) between the two points when both locations are set.
  • Uses custom measurement marker icons so the start and end pins are easier to spot.
  • Keeps the layout polished with spacing around the logo, button, and map.

Behavior

The behavior listens for map clicks, manages the temporary measure pins, and clears the graphics when measure mode is turned off. It also wires the pin click command for normal cabin markers.

using MauiMapAppDemo.ViewModels;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;
using System.Windows.Input;

namespace MauiMapAppDemo.Behaviors
{
    public class MapPinsBehavior : Behavior<Microsoft.Maui.Controls.Maps.Map>
    {
        public static readonly BindableProperty IsMeasuringModeProperty =
            BindableProperty.Create(
                nameof(IsMeasuringMode),
                typeof(bool),
                typeof(MapPinsBehavior),
                false,
                propertyChanged: OnMeasurementStateChanged);

        private void RefreshMeasurementLine()
        {
            if (_map == null)
            {
                return;
            }

            ClearMeasurementGraphics();

            if (MeasureStart == null || MeasureEnd == null)
            {
                return;
            }

            _startPin = new MeasurementPin
            {
                Label = "Start",
                Address = "Measurement Start",
                Location = MeasureStart,
                IconResourceName = "startmarkerv2"
            };

            _endPin = new MeasurementPin
            {
                Label = "End",
                Address = "Measurement End",
                Location = MeasureEnd,
                IconResourceName = "endmarkerv2"
            };
        }
    }
}

View model

The view model owns the state: measurement mode, the first and second tapped locations, the computed distance, and the cabin pins. That keeps the behavior focused on map rendering and interaction.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

namespace MauiMapAppDemo.ViewModels
{
    public partial class MapsViewModel : ObservableObject
    {
        [ObservableProperty]
        public bool _isMeasuringMode;

        [ObservableProperty]
        private Location? _firstLocationMeasureMode;

        [ObservableProperty]
        private Location? _secondLocationMeasureMode;

        [ObservableProperty]
        private double _distanceMeasuredKm;

        public ObservableCollection<MapPinModel> CabinPins { get; } = [];

        [RelayCommand]
        private async Task MapClicked(Location location)
        {
            if (IsMeasuringMode)
            {
                HandleMeasuringMode(location);
                return;
            }

            await HandleDefaultMapClicked(location);
        }
    }
}

Marker icons πŸ“

The biggest visual improvement was the measurement markers. The solution now uses small transparent SVG assets for the start and end points, which avoids oversized default-looking icons and keeps the map readable.

The important part was keeping those marker assets compact and making sure the Android handler only applies them to the measurement pins.

Layout cleanup

The page also got a small spacing pass: the button gained more breathing room and a larger corner radius, the logo and map got margins, and the Eudem-25 info label was moved higher so the top of the page reads more naturally.

MAUI Maps Demo - Code Details

This is a compact code detail listing for the two core pieces behind the demo: the map behavior and the view model. It is meant as a quick reference for how the map, measurement flow, and pin handling fit together.

MapPinsBehavior

The behavior owns map interaction, pin rendering, measurement graphics, and the custom marker icon hookup used for the start and end measurement pins.

using MauiMapAppDemo.ViewModels;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;
using System.Windows.Input;

namespace MauiMapAppDemo.Behaviors
{

    public class MapPinsBehavior : Behavior<Microsoft.Maui.Controls.Maps.Map>
    {

        private Microsoft.Maui.Controls.Maps.Map? _map;

        private Microsoft.Maui.Controls.Maps.Polyline? _measurementLine;

        private Microsoft.Maui.Controls.Maps.Pin? _startPin;
        private Microsoft.Maui.Controls.Maps.Pin? _endPin;


        public static readonly BindableProperty IsMeasuringModeProperty =
            BindableProperty.Create(
                nameof(IsMeasuringMode),
                typeof(bool),
                typeof(MapPinsBehavior),
                false,
                propertyChanged: OnMeasurementStateChanged);

        public bool IsMeasuringMode
        {
            get => (bool)GetValue(IsMeasuringModeProperty);
            set => SetValue(IsMeasuringModeProperty, value);
        }


        public static readonly BindableProperty MeasureStartProperty =
            BindableProperty.Create(
                nameof(MeasureStart),
                typeof(Location),
                typeof(MapPinsBehavior),
                propertyChanged: OnMeasurementChanged
            );

        public Location MeasureStart
        {
            get => (Location)GetValue(MeasureStartProperty);
            set => SetValue(MeasureStartProperty, value);
        }

        public static readonly BindableProperty MeasureEndProperty =
            BindableProperty.Create(
                nameof(MeasureEnd),
                typeof(Location),
                typeof(MapPinsBehavior),
                propertyChanged: OnMeasurementChanged
            );

        public Location MeasureEnd
        {
            get => (Location)GetValue(MeasureEndProperty);
            set => SetValue(MeasureEndProperty, value);
        }


        public static readonly BindableProperty CenterProperty =
         BindableProperty.Create(
             nameof(Center),
             typeof(Location),
             typeof(MapPinsBehavior),
             propertyChanged: OnCenterChanged
             );

        public Location Center
        {
            get => (Location)GetValue(CenterProperty);
            set => SetValue(CenterProperty, value);
        }

        public static readonly BindableProperty PinItemsProperty =
            BindableProperty.Create(
                nameof(PinItems),
                typeof(IEnumerable<MapPinModel>),
                typeof(MapPinsBehavior),
                propertyChanged: OnPinItemsChanged
                );      

        public IEnumerable<MapPinModel>? PinItems
        {
            get => (IEnumerable<MapPinModel>?)GetValue(PinItemsProperty);
            set => SetValue(PinItemsProperty, value);
        }

        public static readonly BindableProperty MapClickedCommandProperty =
            BindableProperty.Create(
                nameof(MapClickedCommand),
                typeof(ICommand),
                typeof(MapPinsBehavior),
                defaultValue: null);

        public ICommand MapClickedCommand
        {
            get => (ICommand)GetValue(MapClickedCommandProperty);
            set => SetValue(MapClickedCommandProperty, value);
        }

        public static readonly BindableProperty PinClickedCommandProperty =
           BindableProperty.Create(
               nameof(PinClickedCommand),
               typeof(ICommand),
               typeof(MapPinsBehavior),
               defaultValue: null);

        public ICommand PinClickedCommand
        {
            get => (ICommand)GetValue(PinClickedCommandProperty);
            set => SetValue(PinClickedCommandProperty, value);
        }

        protected override void OnAttachedTo(Microsoft.Maui.Controls.Maps.Map bindable)
        {
            _map = bindable;

            WireUpMapClickedCommand(bindable);

            base.OnAttachedTo(bindable);

            RefreshPins();

            if (Center is not null)
            {
                _map.MoveToRegion(MapSpan.FromCenterAndRadius(Center, Distance.FromKilometers(8)));
            }
        }

        private void WireUpMapClickedCommand(Microsoft.Maui.Controls.Maps.Map map)
        {
            map.MapClicked += (object? sender, MapClickedEventArgs e) =>
            {
                if (MapClickedCommand?.CanExecute(e.Location) == true)
                {
                    MapClickedCommand.Execute(e.Location);
                }
            };
        }

        protected override void OnDetachingFrom(Microsoft.Maui.Controls.Maps.Map bindable)
        {
            _map = null;

            base.OnDetachingFrom(bindable);
        }

        private static void OnCenterChanged(
            BindableObject bindable,
            object oldValue,
            object newValue)
        {
            var behavior = (MapPinsBehavior)bindable;

            if (behavior._map is not null && newValue is Location location)
            {
                behavior._map.MoveToRegion(MapSpan.FromCenterAndRadius(location,
                    Distance.FromKilometers(8)));
            }
        }

        private static void OnMeasurementChanged(
            BindableObject bindable,
            object oldValue,
            object newValue)
        {
            ((MapPinsBehavior)bindable).RefreshMeasurementLine();
        }


        private static void OnMeasurementStateChanged(
            BindableObject bindable,
            object oldValue,
            object newValue)
        {
            var behavior = (MapPinsBehavior)bindable;

            if (!(bool)newValue)
            {
                behavior.ClearMeasurementGraphics();
            }
        }


        private void RefreshMeasurementLine()
        {
            if (_map == null)
            {
                return;
            }

            ClearMeasurementGraphics();

            if (MeasureStart == null)
            {
                return;
            }
            else
            {
                _startPin = new MeasurementPin
                {
                    Label = "Start",
                    Address = "Measurement Start",
                    Location = MeasureStart,
                    IconResourceName = "startmarkerv2"
                };

                _map.Pins.Add(_startPin);
            }

            if (MeasureEnd == null)
            {
                return;
            }
            else
            {


                _endPin = new MeasurementPin
                {
                    Label = "End",
                    Address = "Measurement End",
                    Location = MeasureEnd,
                    IconResourceName = "endmarkerv2"
                };

                _map.Pins.Add(_endPin);
            }

            _measurementLine = new Polyline
            {
                StrokeColor = Colors.Red,
                StrokeWidth = 5
            };

            _measurementLine.Geopath.Add(MeasureStart);
            _measurementLine.Geopath.Add(MeasureEnd);

            _map.MapElements.Add(_measurementLine);
        }

        private static void OnPinItemsChanged(
            BindableObject bindable,
            object oldValue,
            object newValue)
        {
            ((MapPinsBehavior)bindable).RefreshPins();
        }

        private void ClearMeasurementGraphics()
        {
            if (_map == null)
            {
                return;
            }

            if (_startPin != null)
            {
                _map.Pins.Remove(_startPin);
            }

            if (_endPin != null)
            {
                _map.Pins.Remove(_endPin);
            }

            if (_measurementLine != null)
            {
                _map.MapElements.Remove(_measurementLine);
            }

            _startPin = null;
            _endPin = null;
            _measurementLine = null;
        }

        private void RefreshPins()
        {
            if (_map is null || PinItems is null)
                return;

            _map.Pins.Clear();

            foreach (var item in PinItems.OfType<MapPinModel>())
            {
                var pin = new Pin
                {
                    Label = item.Label,
                    Address = item.Address,
                    Location = new Location(
                        item.Latitude,
                        item.Longitude)
                };

                pin.MarkerClicked += (_, _) =>
                {
                    if (PinClickedCommand?.CanExecute(item) == true)
                    {
                        PinClickedCommand.Execute(item);
                    }
                };

                _map.Pins.Add(pin);
            }
        }




    }
}

MapsViewModel

The view model owns the measurement state, the list of cabin pins, and the commands invoked by the page and the behavior.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiMapAppDemo.Repositories.PinLocations;
using MauiMapAppDemo.Services;
using System.Collections.ObjectModel;

namespace MauiMapAppDemo.ViewModels
{

    public partial class MapsViewModel : ObservableObject
    {
        private readonly OpenTopoService _openTopoService;
        private readonly GeocodingService _geocodingService;
        private readonly DialogService _dialogService;

        private bool _pinClickInProgress = false;

        [ObservableProperty]
        public bool _isMeasuringMode;

        [ObservableProperty]
        private Location? _firstLocationMeasureMode;

        [ObservableProperty]
        private Location? _secondLocationMeasureMode;

        [ObservableProperty]
        private double _distanceMeasuredKm;

        public ObservableCollection<MapPinModel> CabinPins { get; } = [];

        public Location MapCenter { get; } = new(63.4305, 10.3951);

        public MapsViewModel(OpenTopoService openTopoService, GeocodingService geocodingService, DialogService dialogService)
        {
            InitCabinPins();

            _openTopoService = openTopoService;
            _geocodingService = geocodingService;
            _dialogService = dialogService;
        }

        [RelayCommand]
        private async Task PinClicked(MapPinModel pin)
        {
            _pinClickInProgress = true;
            try
            {
                var elevation =
                    await _openTopoService.GetElevationAsync(
                        pin.Latitude,
                        pin.Longitude);

                var placementInfo =
                    await _geocodingService.GetGeocodingPlacemark(
                        pin.Latitude,
                        pin.Longitude);

                await _dialogService.ShowAlertAsync(
                    pin.Label,
                    $"{pin.Address}\n\nElevation: {elevation}m\n\n{placementInfo}",
                    "OK");
            }
            finally
            {
                _pinClickInProgress = false;
            }
        }

        [RelayCommand]
        private async Task ToggleMeasureMode()
        {
            IsMeasuringMode = !IsMeasuringMode; //toggle the measuring mode
        }

        [RelayCommand]
        private async Task MapClicked(Location location)
        {
            if (_pinClickInProgress)
            {
                return;
            }

            if (IsMeasuringMode)
            {
                HandleMeasuringMode(location);
                return;
            }

            await HandleDefaultMapClicked(location);
        }

        private void HandleMeasuringMode(Location location)
        {
            if (FirstLocationMeasureMode == null)
            {
                FirstLocationMeasureMode = location;
                return;
            }

            if (SecondLocationMeasureMode == null)
            {
                SecondLocationMeasureMode = location;

                var distance = Location.CalculateDistance(
                    FirstLocationMeasureMode,
                    SecondLocationMeasureMode,
                    DistanceUnits.Kilometers
                    );

                DistanceMeasuredKm = Math.Round(distance, 1);

                return;
            }

            //Third click restarts over 
            FirstLocationMeasureMode = location;
            SecondLocationMeasureMode = null;
        }      

        private async Task HandleDefaultMapClicked(Location location)
        {
            var elevationOfPoint = await _openTopoService.GetElevationAsync(location.Latitude, location.Longitude);
            await ShowLocationInformationAlert($"Clicked point in the map:", $"Showing elevation of clicked point:", location.Latitude, location.Longitude);
        }

        private async Task ShowLocationInformationAlert(string label, string address, double latitude, double longitude)
        {
            var elevationOfPoint = await _openTopoService.GetElevationAsync(latitude, longitude);

            var placementInfo = await _geocodingService.GetGeocodingPlacemark(latitude, longitude);

            await _dialogService.ShowAlertAsync(
                    label,
                    address + $"\n\nElevation: {elevationOfPoint} m\n\nGeocoding (Placement) info:\n {placementInfo ?? ""}",
                    "OK"
                ); //on click , alert the pin data also via this marker clicked callback 
        }

        private void InitCabinPins()
        {
            foreach (var cabin in TrondheimCabins.GetSampleData())
            {
                CabinPins.Add(
                    new MapPinModel
                    {
                        Label = cabin.Name,
                        Address = cabin.Description,
                        Latitude = cabin.Latitude,
                        Longitude = cabin.Longitude
                    });
            }
        }

    }
}

MauiProgram Adjustments for custom markers on Android platform with Maui

This section shows the startup wiring that makes the demo work on Android: it registers MAUI Maps, reads the maps keys from user secrets, and appends the custom marker icon mapping used by the measurement pins.

using CommunityToolkit.Maui;
using MauiMapAppDemo.Behaviors;
using MauiMapAppDemo.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Maps.Handlers;

namespace MauiMapAppDemo
{
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .UseMauiMaps()
                .UseMauiCommunityToolkit()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });

#if ANDROID
            MapPinHandler.Mapper.AppendToMapping("MeasurementPinIcons", (handler, mapPin) =>
            {
                if (mapPin is not MeasurementPin measurementPin)
                {
                    return;
                }

                var resourceId = GetDrawableResourceId(measurementPin.IconResourceName);
                if (resourceId == 0)
                {
                    return;
                }

                handler.PlatformView.SetIcon(Android.Gms.Maps.Model.BitmapDescriptorFactory.FromResource(resourceId));
            });
#endif

#if DEBUG
            builder.Configuration.AddUserSecrets<App>();
#endif

            string azureMapsKey = builder.Configuration["AzureMapsKey"] ?? string.Empty;
            builder.ConfigureEssentials(essentials => essentials.UseMapServiceToken(azureMapsKey));

#if DEBUG
            builder.Logging.AddDebug();
#endif

            builder.Services.AddSingleton<OpenTopoService>();
            builder.Services.AddSingleton<GeocodingService>();
            builder.Services.AddSingleton<DialogService>();

            return builder.Build();
        }

#if ANDROID
        private static int GetDrawableResourceId(string resourceName)
        {
            if (string.IsNullOrWhiteSpace(resourceName))
            {
                return 0;
            }

            var field = typeof(Resource.Drawable).GetField(
                resourceName,
                System.Reflection.BindingFlags.Public |
                System.Reflection.BindingFlags.Static |
                System.Reflection.BindingFlags.IgnoreCase);

            if (field?.GetValue(null) is int resourceId)
            {
                return resourceId;
            }

            return 0;
        }
#endif
    }
}

Screenshots of the demo running with Android emulator from VS 2026 πŸ“Έ

Here are images of the demo running with the Android emulator. Pixel 7 with API v36 is being used here.

Sunday, 3 May 2026

Inspecting String Length Constraints in EF Core

Inspecting String Length Constraints in EF Core

Entity Framework Core exposes a rich representation of your database schema through its model metadata. This metadata reflects not only attributes on your entities, but also Fluent API configuration, conventions, and provider‑specific decisions.

In this post, we’ll look at a practical way to extract string length constraints from the EF Core model using:

  • C#
  • EF Core 8
  • LINQPad 8

The goal is to surface, programmatically:

  • 🧩 Entity name
  • 🧱 Property name
  • 🏷 Database column name
  • πŸ“ Configured maximum string length (if any)

All of this is done without reflection and without querying database system tables.


Why Use EF Core Metadata?

It’s tempting to reach for reflection and scan attributes like [MaxLength] or [StringLength]. However, reflection only tells you what was declared — not what EF Core ultimately built.

EF Core metadata reflects the resolved model, which may include:

  • ✅ Fluent API overrides
  • ✅ Convention‑based lengths
  • ✅ Provider defaults
  • ✅ Shadow properties
  • ✅ Mappings that never existed as CLR attributes

If EF Core generated it, the metadata knows about it.


Working in LINQPad 8

When you select an EF Core connection in LINQPad, LINQPad instantiates the DbContext for you. Within the query, that context instance is available via this.

This makes LINQPad an excellent environment for exploratory tooling and schema inspection.

Entry point


void Main()
{    
    var dbContext = (DbContext)this;
    
    Console.WriteLine("----------------- GET ALL THE ENTITY TYPE FIELDS/COLUMNS WITH A STRING LENGTH CONSTRAINT------------");
    
    var stringlengthConstrainedField = dbContext.GetAllStringPropertyLengths();

    foreach (var constrainedField in stringlengthConstrainedField)
    {
        Console.WriteLine($"{constrainedField.EntityName} {constrainedField.PropertyName} {constrainedField.ColumnName} {constrainedField.MaxLength}");
    }
    
    
    Console.WriteLine("-------------- GET SPECIFIC ENTITY TYPE's FIELDS/COLUMS WITH A STRING LENGTH CONSTRAINT---------------");
    
    var constrainedFieldsInSpecificTable = dbContext.GetStringPropertyLengths<Users>();

    foreach (var constrainedField in constrainedFieldsInSpecificTable)
    {
        Console.WriteLine($"{constrainedField.EntityName} {constrainedField.PropertyName} {constrainedField.ColumnName} {constrainedField.MaxLength}");
    }
}

πŸ”Ž LINQPad’s immediacy makes this kind of inspection extremely efficient.


DbContext Extension Methods

To keep the querying logic reusable and unobtrusive, the functionality is implemented as DbContext extension methods.

Enumerating all constrained string properties


public static class DbContextExtensions
{
    public static IEnumerable<(string EntityName, string PropertyName, string ColumnName, int? MaxLength)>
        GetAllStringPropertyLengths(this DbContext context, Type? specificEntityType = null)
    {
        var model = context.Model;
        
        IEntityType? specificEntity = null;
        
        if (specificEntityType != null)
        {
            specificEntity = model.FindEntityType(specificEntityType);
        }
        
        if (specificEntityType != null && specificEntity == null)
        {
            yield break;
        }

        var entityTypes = specificEntity == null
            ? model.GetEntityTypes()
            : new[] { specificEntity };
        
        
        foreach (var entityType in entityTypes)
        {
            var clrType = entityType.ClrType;

            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType != typeof(string))
                    continue;

                var maxLength = property.GetMaxLength();
                if (maxLength == null)
                    continue;

                var propertyName = property.Name;
                var columnName = property.GetColumnName();

                yield return (clrType.Name, propertyName, columnName, maxLength);
            }
        }
    }

🧠 This method reports what EF Core actually enforces, not merely what was declared.


Strongly‑typed convenience overload


    public static IEnumerable<(string EntityName, string PropertyName, string ColumnName, int? MaxLength)>
        GetStringPropertyLengths<TModel>(this DbContext context)
    {
        var stringPropertyLenghtsForType =
            GetAllStringPropertyLengths(context, typeof(TModel));

        return stringPropertyLenghtsForType;
    }
}

🎯 This keeps call‑sites expressive without duplicating logic.


Where This Is Useful

  • ✅ Generating client‑side validation rules
  • ✅ Auditing schema constraints in large models
  • ✅ Verifying legacy databases after reverse engineering
  • ✅ Debugging unexpected truncation or validation errors
  • ✅ Building internal tooling around EF Core models

Because it relies on EF Core metadata, it remains accurate across migrations and configuration changes.


Closing Thoughts

EF Core already builds a comprehensive semantic model of your database schema. Exposing and inspecting that model directly is often simpler—and more reliable— than round‑tripping through reflection or database metadata tables.

LINQPad provides a particularly effective environment for this kind of work: quick, focused, and transparent.

πŸ“Œ If you’re already using EF Core, you likely don’t need more tooling — you just need to look at the right layer.