Menu

Creating a C#/ .net service to do Google Places API look up

The application I am working on requires address autocomplete feature, there are few paid services that do this already, but I choose Google Places API – its free (well I don’t expect to eat up quota – my app no way near the limitation).

Google Places API does not return the postcode on the “autocomplete” endpoint (at least to my knowledge – according to documentation), therefore you will need to to another GET request by the Place ID to get the postcode.

I am going to use my favorite packages to help with this development, Newtonsoft.Json and AutoMapper.

Below are the JSON objects

    public class PlacePrediction
    {
        [JsonProperty("place_id")]
        public string PlaceId { get; set; }

        [JsonProperty("description")]
        public string Description { get; set; }

        [JsonProperty("terms")]
        public List<PlacePredictionTerm> Terms { get; set; }
    }

    public class PlacePredictionTerm
    {
        [JsonProperty("offset")]
        public int Offset { get; set; }
        [JsonProperty("value")]
        public string Value { get; set; }
    }

    public class GooglePlacesAutocompleteResult<T>
    {
        [JsonProperty("predictions")]
        public List<T> Places { get; set; }

        [JsonProperty("status")]
        public string Status { get; set; }
    }

    public class GooglePlacesDetailsResult
    {
        [JsonProperty("result")]
        public GooglePlacesSingleResult Result { get; set; }

        [JsonProperty("status")]
        public string Status { get; set; }
    }
    public class GooglePlacesSingleResult
    {
        [JsonProperty("place_id")]
        public string PlaceId { get; set; }

        [JsonProperty("address_components")]
        public List<GooglePlacesAddressComponent> AddressComponents { get; set; }

        [JsonProperty("geometry")]
        public GooglePlacesGeometry Geometry { get; set; }

    }

    public class GooglePlacesAddressComponent
    {
        [JsonProperty("long_name")]
        public string LongName { get; set; }

        [JsonProperty("short_name")]
        public string ShortName { get; set; }

        [JsonProperty("types")]
        public List<string> Types { get; set; }

    }

    public class GooglePlacesGeometry
    {
        [JsonProperty("location")]
        public GooglePlacesLocation Location { get; set; }
    }

    public class GooglePlacesLocation
    {
        [JsonProperty("lat")]
        public double Latitude { get; set; }

        [JsonProperty("lng")]
        public double Longitude { get; set; }
    }

Automapper profile

public class MappingProfile : Profile
    {
        private const string TypesLocality = "locality";
        private const string TypesPolitical = "political";
        private const string TypesCountry = "country";
        private const string TypesPostalCode = "postal_code";
        private const string TypesAdministrativeAreaLevel1 = "administrative_area_level_1";

        public MappingProfile()
        {
            //TODO: I will need to improve this mapping like the 2nd, I will post it here anyway
            CreateMap<PlacePrediction, PlaceEnvelope>(MemberList.None)
                .ForMember(dest => dest.Suburb, expression => expression.ResolveUsing(s => s.Terms?.FirstOrDefault()?.Value))
                .ForMember(dest => dest.State, expression => expression.ResolveUsing(s => s.Terms?.Skip(1).FirstOrDefault()?.Value))
                .ForMember(dest => dest.Country, expression => expression.ResolveUsing(s => s.Terms?.LastOrDefault()?.Value));
            
            CreateMap<GooglePlacesSingleResult, PlaceDetailsEnvelope>(MemberList.None)
                .ForMember(dest => dest.Suburb, opt => opt.ResolveUsing<SuburbResolver>())
                .ForMember(dest => dest.State, opt => opt.ResolveUsing<StateResolver>())
                .ForMember(dest => dest.Country, opt => opt.ResolveUsing<CountryResolver>())
                .ForMember(dest => dest.Postcode, opt => opt.ResolveUsing<PostcodeResolver>())
                .ForMember(dest => dest.Latitude, expression => expression.ResolveUsing(s => s.Geometry.Location.Latitude))
                .ForMember(dest => dest.Longitude, expression => expression.ResolveUsing(s => s.Geometry.Location.Longitude));
        }

        #region CustomResolvers
        public class SuburbResolver : IValueResolver<GooglePlacesSingleResult, PlaceDetailsEnvelope, string>
        {
            public string Resolve(GooglePlacesSingleResult source, PlaceDetailsEnvelope destination, string member, ResolutionContext context)
            {
                return source.AddressComponents.SingleOrDefault(o => o.Types.Contains(TypesLocality) && o.Types.Contains(TypesPolitical))?.LongName;
            }
        }

        public class CountryResolver : IValueResolver<GooglePlacesSingleResult, PlaceDetailsEnvelope, string>
        {
            public string Resolve(GooglePlacesSingleResult source, PlaceDetailsEnvelope destination, string member, ResolutionContext context)
            {
                return source.AddressComponents.SingleOrDefault(o => o.Types.Contains(TypesCountry) && o.Types.Contains(TypesPolitical))?.LongName;
            }
        }

        public class PostcodeResolver : IValueResolver<GooglePlacesSingleResult, PlaceDetailsEnvelope, string>
        {
            public string Resolve(GooglePlacesSingleResult source, PlaceDetailsEnvelope destination, string member, ResolutionContext context)
            {
                return source.AddressComponents.SingleOrDefault(o => o.Types.Contains(TypesPostalCode))?.LongName;
            }
        }

        public class StateResolver : IValueResolver<GooglePlacesSingleResult, PlaceDetailsEnvelope, string>
        {
            public string Resolve(GooglePlacesSingleResult source, PlaceDetailsEnvelope destination, string member, ResolutionContext context)
            {
                return source.AddressComponents.SingleOrDefault(o => o.Types.Contains(TypesAdministrativeAreaLevel1) && o.Types.Contains(TypesPolitical))?.LongName;
            }
        }
        #endregion
    }

Here is the IGooglePlacesService Interface.

    public interface IGooglePlacesService
    {
        Task<List<PlacePrediction>> GetAutocompleteAsync(string searchTerm);
        Task<GooglePlacesSingleResult> GetSingleAsync(string placeId);
    }

Here is the implementation

public class GooglePlacesService : IGooglePlacesService
    {
        static readonly HttpClient Client = new HttpClient();
        private readonly GoogleWebServiceSettings _googleWebServiceSettings;
        public GooglePlacesService(IOptions<GoogleWebServiceSettings> googleWebServiceSettings)
        {
            _googleWebServiceSettings = googleWebServiceSettings.Value;
        }
        public async Task<List<PlacePrediction>> GetAutocompleteAsync(string searchTerm)
        {
            var response = await Client.GetAsync($"{_googleWebServiceSettings.PlacesAPIUrl}autocomplete/json?key={_googleWebServiceSettings.PlacesAPIKey}&input={searchTerm}");
            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsAsync<GooglePlacesAutocompleteResult<PlacePrediction>>();
                return content.Places;
            }

            return new List<PlacePrediction>();
        }

        public async Task<GooglePlacesSingleResult> GetSingleAsync(string placeId)
        {
            var response = await Client.GetAsync($"{_googleWebServiceSettings.PlacesAPIUrl}details/json?key={_googleWebServiceSettings.PlacesAPIKey}&placeid={placeId}");
            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsAsync<GooglePlacesDetailsResult>();
                return content.Result;
            }

            return new GooglePlacesSingleResult();
        }
    }

Hope this helps

Leave a comment