Search code examples
c#model-view-controllergeolocationrepository-design

Get results within x miles Using GeoCoordinate with respository pattern


I would like to get a list of all organisations within x miles of a location entered by the user. This is converted to a long/lat location.

Organisations are stored in the database with long and lat.

I am using MVC with entity framework and a Unit of Work with the respository pattern to access the dataset.

Here is my EntityRepository:

        public IQueryable<T> All
    {
        get
        {
            return dbSet;
        }
    }

    public IQueryable<T> AllIncluding(params System.Linq.Expressions.Expression<Func<T, object>>[] includeProperties)
    {
        IQueryable<T> query = dbSet;
        foreach (var includeProperty in includeProperties)
        {
            query = query.Include(includeProperty);
        }

        return query;
    }

    public IEnumerable<T> Where(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
    {
        return dbSet.Where(predicate).AsEnumerable();
    }

To query the data in my datacontext I use a service class for each entity, a UOW is injected into each service. The service call for organisations is:

 public class OrgService :IOrgService
{
    private IUnitOfWork _UoW;

    public OrgService(IUnitOfWork UoW)
    {
        _UoW = UoW;
    }

    public Organisation GetOrgByID(int OrgID)
    {

        return _UoW.OrganisationRepo.Find(OrgID);
    }

    public IList<Organisation> GetAllOrgs()
    {
        return _UoW.OrganisationRepo.All.ToList();  
    }

    public IList<Organisation> GetOrgsByLocation(double lat, double lng, int range)
    {

      /// I need to return a list of all organisations within X miles

    }

 }

All other queries are working as the they should however I am no trying to write the method GetOrgsByLocation(). This is the query I think I need to get my results:

 var userLocation = new GeoCoordinate(lat, lng);
 var result = _UoW.OrganisationRepo.Where(x => new GeoCoordinate(x.Latitude, x.Longitude))
                              .Where(x => x.GetDistanceTo(userLocation) < radius).ToList();

When I try to run this query I get:

"cannot implicitly convert type system.device.location.geoCoordinate to bool"

Can anyone help?

** Update - Working solution **

 var userLocation = new GeoCoordinate(lat, lng);
        var nearbyOrganizations = _UoW.OrganisationRepo.All.ToList()
         .Select(x => new
         { //use an anonymous type or any type you want
             Org = x,
             Distance = new GeoCoordinate(x.Latitude, x.Longitude).GetDistanceTo(userLocation)
         }) 
         .Where(x => x.Distance < 50000)
         .ToList();

        foreach (var organisation in nearbyOrganizations)
        {
            Console.WriteLine("{0} ({1:n0} meters)", organisation.Org, organisation.Distance);
        }

Thanks to the help below for this solution, though it seems all objects have to be queried in orfer for this to work, it seems that query would be better suited to run on the database, I'll have to look into this more.


Solution

  • The Where method has to return a boolean value.

    _UoW.OrganisationRepo.Where(x => new GeoCoordinate(x.Latitude, x.Longitude))
    

    Maybe you meant to use a .Select there? Would the following code work?

    _UoW.OrganisationRepo.Select(x => new GeoCoordinate(x.Latitude, x.Longitude))
                              .Where(x => x.GetDistanceTo(userLocation) < radius).ToList();
    

    Be aware that Entity Framework tries to generate some SQL from the expression provided. I'm afraid that x.GetDistanceTo(userLocation) might not work inside the .Where expression, unless you cast it to an IEnumerable, or call .AsEnumerable() or .ToList() or .ToArray() before calling .Where. Or maybe EF is smart enough to see that GeoCoordinate is not mapped to a table, and then stop generating the SQL right there.

    Edit

    The code you commented won't work:

    _UoW.OrganisationRepo.All.Select(x => new GeoCoordinate(x.Latitude, x.Longitude).GetDistanceTo(userLocation) < radius).ToList()
    

    Notice that you're selecting a list of bools because you're selecting the results instead of filtering by them. You won't know which organizations are within the radius. That's why we use .Select and .Where separately.

    Try something like this:

    _UoW.OrganisationRepo.All
         .Select(x => new GeoCoordinate(x.Latitude, x.Longitude))
         .ToEnumerable() //or .ToList() or .ToArray(), make sure it's outside of EF's reach (prevent SQL generation for this)
         .Where(x=> x.GetDistanceTo(userLocation) < radius).ToList()
    

    However, if you want to know which organizations are within the radius, you'll need to carry more information along the path.

    var nearbyOrganizations = _UoW.OrganisationRepo.All.ToList()
         .Select(x => new 
          { //use an anonymous type or any type you want
              Org = x, 
              Distance = new GeoCoordinate(x.Latitude, x.Longitude).GetDistanceTo(userLocation) 
         }) //it's probably outside EF's SQL generation step by now, but you could add one more .Select here that does the math if it fails (return the GeoCoordinate object)
         .Where(x=> x.Distance < radius)
         .ToList();
    

    Seems like it would be useful for you to know more about .Where and .Select. They're super useful. .Where is essentialy a filter, and .Select transforms objects. And due to anonymous types, you can do whatever you want.

    Notice that this will fetch all objects from the database. You probably want to use native features of the database to work with geographical data, and EF probably supports it.