Пример #1
0
        /// <summary>
        /// This method will retrieve a list of Businesses from Yelp with separate calls.
        /// However, those calls will be made in parallel so while many calls will be made, the total
        /// results should be fast.
        /// Written in part with: https://stackoverflow.com/a/39796934/311444 and https://stackoverflow.com/a/23316722/311444
        /// </summary>
        /// <param name="businessIds">A list of Yelp Business Ids to request from the GraphQL endpoint.</param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="maxThreads">
        ///     The max amount of calls to be made at one time by SemaphoreSlim. 2 is the recommended amount.
        ///     More threads would mean more calls at once, but a greater chance of getting calls rejected by Yelp.
        /// </param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>Returns an IEnumerable of BusinessResponses for each submitted businessId, wrapped in a Task.</returns>
        public async Task <IEnumerable <BusinessResponse> > GetBusinessAsyncInParallel(
            IEnumerable <string> businessIds,
            CancellationToken ct = default(CancellationToken),
            int maxThreads       = 2,
            ConnectionRetrySettings connectionRetrySettings = null)
        {
            if (connectionRetrySettings == null)
            {
                connectionRetrySettings = new ConnectionRetrySettings();
            }

            SemaphoreSlim semaphoreSlim = new SemaphoreSlim(maxThreads, maxThreads);

            var businessResponses = new List <BusinessResponse>();
            await Task.WhenAll(businessIds.Select(async businessId =>
            {
                await semaphoreSlim.WaitAsync(ct);
                try
                {
                    // You have to create a separate object for each Semaphore, otherwise they all use the same counter and quickly run out of retries.
                    ConnectionRetrySettings connectionRetrySettingsForThisAttempt = new ConnectionRetrySettings(
                        connectionRetrySettings.CurrentTry,
                        connectionRetrySettings.IsRetryConnections,
                        connectionRetrySettings.MaxAmountOfTries);

                    businessResponses.Add(await GetBusinessAsync(businessId, ct, connectionRetrySettingsForThisAttempt));
                }
                finally
                {
                    semaphoreSlim.Release();
                }
            }));

            return(businessResponses);
        }
Пример #2
0
        /// <summary>
        /// Searches businesses matching the specified search text used in a client search autocomplete box.
        /// </summary>
        /// <param name="text">Text to search businesses with.</param>
        /// <param name="latitude">User's current latitude.</param>
        /// <param name="longitude">User's current longitude.</param>
        /// <param name="locale">Language/locale value from https://www.yelp.com/developers/documentation/v3/supported_locales </param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>AutocompleteResponse with businesses/categories/terms matching the specified parameters.</returns>
        public async Task <AutocompleteResponse> AutocompleteAsync(
            string text,
            double latitude,
            double longitude,
            string locale        = null,
            CancellationToken ct = default(CancellationToken),
            ConnectionRetrySettings connectionRetrySettings = null)
        {
            this.ValidateCoordinates(latitude, longitude);
            this.ApplyAuthenticationHeaders(ct);

            var dic = new Dictionary <string, object>();

            dic.Add("text", text);
            dic.Add("latitude", latitude);
            dic.Add("longitude", longitude);
            if (!string.IsNullOrEmpty(locale))
            {
                dic.Add("locale", locale);
            }
            string querystring = dic.ToQueryString();

            var response = await this.GetAsync <AutocompleteResponse>(API_VERSION + "/autocomplete" + querystring, ct, connectionRetrySettings);

            // Set distances baased on lat/lon
            if (response?.Businesses != null && !double.IsNaN(latitude) && !double.IsNaN(longitude))
            {
                foreach (var business in response.Businesses)
                {
                    business.SetDistanceAway(latitude, longitude);
                }
            }

            return(response);
        }
Пример #3
0
        /// <summary>
        /// Searches businesses that deliver matching the specified search text.
        /// </summary>
        /// <param name="term">Text to search businesses with.</param>
        /// <param name="latitude">User's current latitude.</param>
        /// <param name="longitude">User's current longitude.</param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>SearchResponse with businesses matching the specified parameters.</returns>
        public async Task <SearchResponse> SearchBusinessesWithDeliveryAsync(
            string term,
            double latitude,
            double longitude,
            CancellationToken ct = default(CancellationToken),
            ConnectionRetrySettings connectionRetrySettings = null)
        {
            this.ValidateCoordinates(latitude, longitude);
            this.ApplyAuthenticationHeaders(ct);

            var dic = new Dictionary <string, object>();

            if (!string.IsNullOrEmpty(term))
            {
                dic.Add("term", term);
            }
            dic.Add("latitude", latitude);
            dic.Add("longitude", longitude);
            string querystring = dic.ToQueryString();
            var    response    = await this.GetAsync <SearchResponse>(API_VERSION + "/transactions/delivery/search" + querystring, ct, connectionRetrySettings);

            // Set distances based on lat/lon
            if (response?.Businesses != null && !double.IsNaN(latitude) && !double.IsNaN(longitude))
            {
                foreach (var business in response.Businesses)
                {
                    business.SetDistanceAway(latitude, longitude);
                }
            }

            return(response);
        }
Пример #4
0
        /// <summary>
        /// Searches any and all businesses matching the data in the specified search parameter object.
        /// </summary>
        /// <param name="search">Container object for all search parameters.</param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>SearchResponse with businesses matching the specified parameters.</returns>
        public async Task <SearchResponse> SearchBusinessesAllAsync(
            SearchRequest search,
            CancellationToken ct = default(CancellationToken),
            ConnectionRetrySettings connectionRetrySettings = null)
        {
            if (search == null)
            {
                throw new ArgumentNullException(nameof(search));
            }

            this.ValidateCoordinates(search.Latitude, search.Longitude);
            this.ApplyAuthenticationHeaders(ct);

            var querystring = search.GetChangedProperties().ToQueryString();
            var response    = await this.GetAsync <SearchResponse>(API_VERSION + "/businesses/search" + querystring, ct, connectionRetrySettings);

            // Set distances baased on lat/lon
            if (response?.Businesses != null && !double.IsNaN(search.Latitude) && !double.IsNaN(search.Longitude))
            {
                foreach (var business in response.Businesses)
                {
                    business.SetDistanceAway(search.Latitude, search.Longitude);
                }
            }

            return(response);
        }
Пример #5
0
        /// <summary>
        /// Posts data to the specified URL.
        /// </summary>
        /// <param name="url">URL to retrieve data from.</param>
        /// <param name="ct">Cancellation token.</param>
        /// <param name="httpConnectionSettings">Settings to create the HttpContent value.  Doing it inside the method allows for connection retries.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>Response contents as string else null if nothing.</returns>
        protected async Task <string> PostAsync(
            string url,
            CancellationToken ct,
            HttpConnectionSettings httpConnectionSettings,
            ConnectionRetrySettings connectionRetrySettings = null)
        {
            if (connectionRetrySettings == null)
            {
                connectionRetrySettings = new ConnectionRetrySettings();
            }

            HttpResponseMessage response = await this.PostAsync(
                url,
                new StringContent(httpConnectionSettings.Content, httpConnectionSettings.Encoding, httpConnectionSettings.MediaType),
                ct);

            var data = await response.Content?.ReadAsStringAsync();

            if (DoesThisNeedToRetry(connectionRetrySettings, data, response.StatusCode))
            {
                connectionRetrySettings.CurrentTry++;
                return(await PostAsync(url, ct, httpConnectionSettings, connectionRetrySettings));
            }
            ThrowIfAccessLimitReached(data, response.StatusCode);

            return(data);
        }
Пример #6
0
        /// <summary>
        /// Gets data from the specified URL.
        /// </summary>
        /// <typeparam name="T">Type for the strongly typed class representing data returned from the URL.</typeparam>
        /// <param name="url">URL to retrieve data from.</param>
        /// <param name="ct">Cancellation token.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>Instance of the type specified representing the data returned from the URL.</returns>
        protected async Task <T> GetAsync <T>(string url, CancellationToken ct, ConnectionRetrySettings connectionRetrySettings)
        {
            if (string.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException(nameof(url));
            }

            if (connectionRetrySettings == null)
            {
                connectionRetrySettings = new ConnectionRetrySettings();
            }

            var response = await this.Client.GetAsync(new Uri(this.BaseUri, url), ct);

            this.Log(response);
            var data = await response.Content.ReadAsStringAsync();

            if (DoesThisNeedToRetry(connectionRetrySettings, data, response.StatusCode))
            {
                connectionRetrySettings.CurrentTry++;
                return(await GetAsync <T>(url, ct, connectionRetrySettings));
            }
            ThrowIfAccessLimitReached(data, response.StatusCode);

            var settings = new JsonSerializerSettings
            {
                NullValueHandling     = NullValueHandling.Ignore,
                MissingMemberHandling = MissingMemberHandling.Ignore
            };
            var jsonModel = JsonConvert.DeserializeObject <T>(data, settings);

            return(jsonModel);
        }
Пример #7
0
 /// <summary>
 /// Gets details of a business based on the provided ID value.
 /// </summary>
 /// <param name="businessID">ID value of the Yelp business.</param>
 /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
 /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
 /// <returns>BusinessResponse instance with details of the specified business if found.</returns>
 public async Task <BusinessResponse> GetBusinessAsync(
     string businessID,
     CancellationToken ct = default(CancellationToken),
     ConnectionRetrySettings connectionRetrySettings = null)
 {
     this.ApplyAuthenticationHeaders(ct);
     return(await this.GetAsync <BusinessResponse>(API_VERSION + "/businesses/" + Uri.EscapeUriString(businessID), ct, connectionRetrySettings));
 }
Пример #8
0
 /// <summary>
 /// This method runs in series, for a parallel version please see GetGraphQlAsyncInParallel.
 /// This method will take the list of businessIds and divide them into chunks.
 /// These chunks will be submitted to the GraphQL endpoint separately one after the other.
 /// This will make more calls to the GraphQL endpoint than GetGraphQlAsync, but each call will only be a portion of the
 /// total businesses so it should run faster than GetGraphQlAsync.
 /// *** NOTE ***
 /// The GraphQL endpoint is currently only available in the Yelp Fusion (3.0) Api Beta.
 /// To use these endpoints, you have to go to Manage App and opt into the Beta.
 /// </summary>
 /// <param name="businessIds">A list of Yelp Business Ids to request from the GraphQL endpoint.</param>
 /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
 /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
 /// <param name="chunkSize">
 ///     How many businesses to submit on each request.  25 is the recommended amount.
 ///     Submitting more at one time will make the call to Yelp take longer, but there will be less calls to Yelp overall.
 ///     Submitting less at one time will make the calls to Yelp quicker, but there will be more calls to Yelp overall.
 /// </param>
 /// <param name="fragment">The search fragment to be used on all requested Business Ids.  The DEFAULT_FRAGMENT is used by default.</param>
 /// <param name="maxThreads">
 ///     The max amount of calls to be made at one time by SemaphoreSlim. 2 is the recommended amount.
 ///     More threads would mean more calls at once, but a greater chance of getting calls rejected by Yelp.
 /// </param>
 /// <returns>A list of all BusinessResponses returned by every call to the GraphQL endpoint.</returns>
 public async Task <IEnumerable <BusinessResponse> > GetGraphQlInChunksAsync(
     List <string> businessIds,
     CancellationToken ct = default(CancellationToken),
     ConnectionRetrySettings connectionRetrySettings = null,
     int chunkSize   = 25,
     string fragment = DEFAULT_FRAGMENT)
 {
     return(await GetGraphQlInChunksAsyncInParallel(businessIds, ct, connectionRetrySettings, chunkSize, fragment, 1));
 }
Пример #9
0
        /// <summary>
        /// Gets data from the specified URL.
        /// </summary>
        /// <typeparam name="T">Type for the strongly typed class representing data returned from the URL.</typeparam>
        /// <param name="url">URL to retrieve data from.</param>
        /// <param name="ct">Cancellation token.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>Instance of the type specified representing the data returned from the URL.</returns>
        protected async Task <T> GetAsync <T>(string url, CancellationToken ct, ConnectionRetrySettings connectionRetrySettings) where T : ResponseBase
        {
            if (string.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException(nameof(url));
            }

            if (connectionRetrySettings == null)
            {
                connectionRetrySettings = new ConnectionRetrySettings();
            }

            var response = await this.Client.GetAsync(new Uri(this.BaseUri, url), ct);

            this.Log(response);
            var data = await response.Content.ReadAsStringAsync();

            if (DoesThisNeedToRetry(connectionRetrySettings, data, response.StatusCode))
            {
                connectionRetrySettings.CurrentTry++;
                return(await GetAsync <T>(url, ct, connectionRetrySettings));
            }
            ThrowIfAccessLimitReached(data, response.StatusCode);

            var settings = new JsonSerializerSettings
            {
                NullValueHandling     = NullValueHandling.Ignore,
                MissingMemberHandling = MissingMemberHandling.Ignore
            };
            var jsonModel = JsonConvert.DeserializeObject <T>(data, settings);

            jsonModel.RateLimit = new RateLimit();
            if (response.Headers.Contains("RateLimit-DailyLimit"))
            {
                jsonModel.RateLimit.DailyLimit = Convert.ToInt32(response.Headers.GetValues("RateLimit-DailyLimit").FirstOrDefault());
            }
            if (response.Headers.Contains("RateLimit-Remaining"))
            {
                jsonModel.RateLimit.Remaining = Convert.ToInt32(response.Headers.GetValues("RateLimit-Remaining").FirstOrDefault());
            }
            if (response.Headers.Contains("RateLimit-ResetTime"))
            {
                jsonModel.RateLimit.ResetTime = Convert.ToDateTime(response.Headers.GetValues("RateLimit-ResetTime").FirstOrDefault());
            }

            return(jsonModel);
        }
Пример #10
0
        /// <summary>
        /// Searches any and all businesses matching the specified search text.
        /// </summary>
        /// <param name="term">Text to search businesses with.</param>
        /// <param name="latitude">User's current latitude.</param>
        /// <param name="longitude">User's current longitude.</param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>SearchResponse with businesses matching the specified parameters.</returns>
        public Task <SearchResponse> SearchBusinessesAllAsync(
            string term,
            double latitude,
            double longitude,
            CancellationToken ct = default(CancellationToken),
            ConnectionRetrySettings connectionRetrySettings = null)
        {
            SearchRequest search = new SearchRequest();

            if (!string.IsNullOrEmpty(term))
            {
                search.Term = term;
            }
            search.Latitude  = latitude;
            search.Longitude = longitude;
            return(this.SearchBusinessesAllAsync(search, ct, connectionRetrySettings));
        }
Пример #11
0
        /// <summary>
        /// Gets user reviews of a business based on the provided ID value.
        /// </summary>
        /// <param name="businessID">ID value of the Yelp business.</param>
        /// <param name="locale">Language/locale value from https://www.yelp.com/developers/documentation/v3/supported_locales </param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <returns>ReviewsResponse instance with reviews of the specified business if found.</returns>
        public async Task <ReviewsResponse> GetReviewsAsync(
            string businessID,
            string locale        = null,
            CancellationToken ct = default(CancellationToken),
            ConnectionRetrySettings connectionRetrySettings = null)
        {
            this.ApplyAuthenticationHeaders(ct);

            var dic = new Dictionary <string, object>();

            if (!string.IsNullOrEmpty(locale))
            {
                dic.Add("locale", locale);
            }
            string querystring = dic.ToQueryString();

            return(await this.GetAsync <ReviewsResponse>(API_VERSION + $"/businesses/{Uri.EscapeUriString(businessID)}/reviews" + querystring, ct, connectionRetrySettings));
        }
Пример #12
0
        private bool DoesThisNeedToRetry(ConnectionRetrySettings connectionRetrySettings, string content, HttpStatusCode responseCode)
        {
            // TODO: 429 Too Many Requests was not included in .NET Core 1.0.  Change when upgrading to 2.0
            // TODO: Look into using this instead in 2.1 https://stackoverflow.com/a/35183487/311444
            if (Convert.ToInt32(responseCode) == 429)
            {
                if (content.Contains("You have exceeded the queries-per-second limit for this endpoint"))
                {
                    if (connectionRetrySettings.IsRetryConnections &&
                        connectionRetrySettings.CurrentTry <= connectionRetrySettings.MaxAmountOfTries)
                    {
                        return(true);
                    }
                }
            }

            return(false);
        }
Пример #13
0
        /// <summary>
        /// This method runs in parallel, for a version that runs in series please see GetGraphQlInChunksAsync.
        /// This method will take the list of businessIds and divide them into chunks.
        /// They will be submitted in parallel as defined by the size of the SemaphoreSlim.
        /// This will make more calls to the GraphQL endpoint than GetGraphQlAsync, but each call will only be a portion of the
        /// total businesses so it should run faster than GetGraphQlAsync.
        /// The calls are done in parallel so it'll be faster than both GetGraphQlAsync and GetGraphQlInChunksAsync.
        /// Written in part with: https://stackoverflow.com/a/39796934/311444 and https://stackoverflow.com/a/23316722/311444
        /// *** NOTE ***
        /// The GraphQL endpoint is currently only available in the Yelp Fusion (3.0) Api Beta.
        /// To use these endpoints, you have to go to Manage App and opt into the Beta.
        /// </summary>
        /// <param name="businessIds">A list of Yelp Business Ids to request from the GraphQL endpoint.</param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <param name="chunkSize">
        ///     How many businesses to submit on each request.  25 is the recommended amount.
        ///     Submitting more at one time will make the call to Yelp take longer, but there will be less calls to Yelp overall.
        ///     Submitting less at one time will make the calls to Yelp quicker, but there will be more calls to Yelp overall.
        /// </param>
        /// <param name="fragment">The search fragment to be used on all requested Business Ids.  The DEFAULT_FRAGMENT is used by default.</param>
        /// <param name="maxThreads">
        ///     The max amount of calls to be made at one time by SemaphoreSlim. 2 is the recommended amount.
        ///     More threads would mean more calls at once, but a greater chance of getting calls rejected by Yelp.
        /// </param>
        /// <returns>
        /// A list of Tasks where each Task contains an IEnumerable of BusinessResponses.  The caller will have to await for the Tasks
        /// to return to get the results.
        /// </returns>
        public async Task <IEnumerable <BusinessResponse> > GetGraphQlInChunksAsyncInParallel(
            List <string> businessIds,
            CancellationToken ct = default(CancellationToken),
            ConnectionRetrySettings connectionRetrySettings = null,
            int chunkSize   = 25,
            string fragment = DEFAULT_FRAGMENT,
            int maxThreads  = 2)
        {
            if (connectionRetrySettings == null)
            {
                connectionRetrySettings = new ConnectionRetrySettings();
            }

            SemaphoreSlim semaphoreSlim = new SemaphoreSlim(maxThreads, maxThreads);

            var businessResponses = new List <BusinessResponse>();
            var businessSubsets   = GetSubsetsOfBusinessIds(businessIds, chunkSize);
            await Task.WhenAll(businessSubsets.Select(async subset =>
            {
                await semaphoreSlim.WaitAsync(ct);
                try
                {
                    // You have to create a separate object for each Semaphore, otherwise they all use the same counter and quickly run out of retries.
                    ConnectionRetrySettings connectionRetrySettingsForThisAttempt = new ConnectionRetrySettings(
                        connectionRetrySettings.CurrentTry,
                        connectionRetrySettings.IsRetryConnections,
                        connectionRetrySettings.MaxAmountOfTries);

                    businessResponses.AddRange(await GetGraphQlAsync(businessIds, ct, connectionRetrySettingsForThisAttempt, fragment));
                }
                finally
                {
                    semaphoreSlim.Release();
                }
            }));

            return(businessResponses);
        }
Пример #14
0
        /*
         * The GraphQL endpoint is currently only available in the Yelp Fusion (3.0) Api Beta.
         * To use these endpoints, you have to go to Manage App and opt into the Beta.
         */

        #region Individual Graph Request

        /// <summary>
        /// This method makes a single request to the Yelp GraphQL endpoint.
        /// It formats the entire list of businessIds and the search fragment into the proper json to make the request.
        /// *** NOTE ***
        /// The GraphQL endpoint is currently only available in the Yelp Fusion (3.0) Api Beta.
        /// To use these endpoints, you have to go to Manage App and opt into the Beta.
        /// </summary>
        /// <param name="businessIds">A list of Yelp Business Ids to request from the GraphQL endpoint.</param>
        /// <param name="ct">Cancellation token instance. Use CancellationToken.None if not needed.</param>
        /// <param name="connectionRetrySettings">The settings to define whether a connection should be retried.</param>
        /// <param name="fragment">The search fragment to be used on all requested Business Ids.  The DEFAULT_FRAGMENT is used by default.</param>
        /// <returns>A task of an IEnumerable of all the BusinessResponses from the GraphQL API.</returns>
        public async Task <IEnumerable <BusinessResponse> > GetGraphQlAsync(
            List <string> businessIds,
            CancellationToken ct = default(CancellationToken),
            ConnectionRetrySettings connectionRetrySettings = null,
            string fragment = DEFAULT_FRAGMENT)
        {
            if (!businessIds.Any())
            {
                return(new List <BusinessResponse>());
            }

            var httpConnectionSettings = new HttpConnectionSettings
            {
                Content   = CreateRequestBodyForGraphQl(businessIds, fragment),
                Encoding  = Encoding.UTF8,
                MediaType = "application/graphql"
            };

            ApplyAuthenticationHeaders(ct);
            var jsonResponse = await PostAsync(API_VERSION + "/graphql", ct, httpConnectionSettings, connectionRetrySettings);

            return(ConvertJsonToBusinesResponses(jsonResponse));
        }