Home > front end >  PetaPoco Fails to Pull Geography Records
PetaPoco Fails to Pull Geography Records

Time:12-16

I'm using C#, .NET framework 4.5 (with intentions to upgrade to .NET 5), and PetaPoco as my ORM.

I have a table named Jurisdiction with the following field definition:

CREATE Table [Jurisdiction]
  ...
  [GeographicArea] [geography] NULL
  ...
);

In my database layer, I have the following:

var sql = @"
    SELECT
        Jurisdiction.*,
        State.StateName
    FROM
        Jurisdiction
        LEFT OUTER JOIN State ON Jurisdiction.StateId = State.StateId
";

if (where.Count > 0)
{
    sql  = $" WHERE {string.Join(" AND ", where)}";
}

sql  = orderBy;


var jurisdictions = _database.Query<T>(sql, parameters.ToArray()).ToList();

However, when this method runs I get the following exception:

'Could not load file or assembly 'Microsoft.SqlServer.Types, Version=10.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' or one of its dependencies. The system cannot find the file specified.'

This was causing an issue when I let the database.tt file automatically generate the POCO definition for Jurisdiction so what I did to the tt file was add the following so that it would stop trying to automatically use the SqlServers.Geography type:

tables["Jurisdiction"]["GeographicArea"].PropertyType="string";

However, even with defining the field as a string, it is still throwing the exception, regardless of if I have the Microsoft.SqlServer.Types library added to the project or not.

How can I can not fool with the Microsoft.SqlServer.Types library using PetaPoco?

CodePudding user response:

I was ultimately able to resolve my issue, though it was very much involved. In my query, instead of doing a SELECT * I had to spell out each column and manually convert the geography value using:

SELECT
    Jurisdiction.JurisdictionId,
    Jurisdiction.CreatedBy,
    Jurisdiction.CreatedOn,
    -- etc...
    CASE WHEN Jurisdiction.GeographicArea IS NULL THEN NULL ELSE Jurisdiction.GeographicArea.ToString() END AS GeographicArea -- CASE/WHEN/THEN/ELSE speeds up query
FROM
    Jurisdiction

Then in my view model, I set the insert and update templates using:

[Column(InsertTemplate = "geography::STMPolyFromText({0}{1}, 4326)", UpdateTemplate = "{0} = geography::STMPolyFromText({1}{2}, 4326)")] new public string GeographicArea { get; set; }

Finally in my service layer, I created the following two methods to get/update the geography column using:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace MyApplication
{
    public class Coordinate
    {
        private float _latitude;
        private float _longitude;

        public float Latitude {
            get => _latitude;
            set {
                if (value < -90 || value > 90)
                {
                    throw new ArgumentOutOfRangeException(nameof(Latitude), "The latitude is not between -90 and 90.");
                }
                _latitude = value;
            }
        }
        public float Longitude
        {
            get => _longitude;
            set
            {
                if (value < -180 || value > 180)
                {
                    throw new ArgumentOutOfRangeException(nameof(Longitude), "The longitude is not between -180 and 180.");
                }
                _longitude = value;
            }
        }

        public Coordinate()
        {
            Latitude = 0;
            Longitude = 0;
        }

        public Coordinate(string latitude, string longitude)
        {
            if (!float.TryParse(latitude, out float latitudeFloat))
            {
                throw new ArgumentException("Latitude must be a valid number.");
            }

            if (!float.TryParse(longitude, out float longitudeFloat))
            {
                throw new ArgumentException("Longitude must be a valid number.");
            }
            Latitude = latitudeFloat;
            Longitude = longitudeFloat;
        }

        public Coordinate(float latitude, float longitude)
        {
            Latitude = latitude;
            Longitude = longitude;
        }
    }

    public class SpatialConverterService
    {
        // find everything but ([^...]): numbers (\d), decimal points (\.), spaces (\s), and commas (,)
        private readonly static Regex _geographyIrrelevantData = new Regex(@"[^\d\.\s\-,]");

        /// <summary>
        /// Takes a SQL geography string and converts it to a collection of Coordinate values
        /// </summary>
        /// <param name="geography"><see cref="string"/> the SQL geography string</param>
        /// <returns><see cref="IEnumerable{Coordinate}"/> the collection of coordinates</returns>
        public static IEnumerable<Coordinate> ConvertSqlGeographyToCoordinates(string geography)
        {
            var geographyPoints = _geographyIrrelevantData.Replace(geography, string.Empty);
            geographyPoints = geographyPoints.Trim();

            var coordinateStrings = geographyPoints.Split(new[] { ',' });
            var coordinates = coordinateStrings.Select(coordinate =>
            {
                coordinate = coordinate.Trim();
                var points = coordinate.Split(new[] { ' ' });
                if (points.Count() != 2)
                {
                    throw new Exception($"Coordinate is not in a valid format, expecting longitude and latitude separated by a space but got: {coordinate}");
                }

                // SQL represents points as: lng lat
                return new Coordinate(points[1], points[0]);
            });

            return coordinates;
        }

        /// <summary>
        /// Takes a collection of <see cref="Coordinate"/> and converts it to a SQL geography string
        /// </summary>
        /// <param name="coordinates"><see cref="IEnumerable{Coordinate}"/> the collection of coordinates to convert</param>
        /// <returns><see cref="string"/> the SQL geography string</returns>
        public static string ConvertCoordinatesToSqlGeography(IEnumerable<Coordinate> coordinates)
        {
            if (!coordinates.Any())
            {
                throw new ArgumentNullException(nameof(coordinates), "There are no coordinates in the collection.");
            }

            var sqlConversion = string.Join(", ", coordinates.Select(coordinate => $"{coordinate.Longitude} {coordinate.Latitude}"));
            if (coordinates.First() != coordinates.Last() || coordinates.Count() == 1)
            {
                // SQL requires that the geography get completed by ending on the first coordinate
                var firstCoordinate = coordinates.First();
                sqlConversion  = $", {firstCoordinate.Longitude} {firstCoordinate.Latitude}";
            }
            
            var multipolygon = $"MULTIPOLYGON((({sqlConversion})))";
            return multipolygon;
        }
    }
}
  • Related