11
\$\begingroup\$

Part of a project I'm getting started on requires access to the Stack Exchange API for certain data, as a result I built a .NET implementation to interact with it.

The implementation is pretty simple, in my opinion. You can also find it on GitHub.


It features a main Configuration class, in which you put a lot of the API configuration information.

/// <summary>
/// Represents a Stack Exchange API configuration for use with API requests.
/// </summary>
public class Configuration
{
    /// <summary>
    /// Represents the base endpoint for the Stack Exchange API url.
    /// </summary>
    public const string ApiUrlBase = "{Protocol}://api.stackexchange.com/{Version}/";

    /// <summary>
    /// This is the upper bound of the Page Size for <b>most</b> requests. Currently 100.
    /// </summary>
    public const int MaxPageSize = 100;

    /// <summary>
    /// The application API key. Can be <code>null</code> for anonymous requests.
    /// </summary>
    public string Key { get; set; }

    /// <summary>
    /// If true then the HTTPS protocol will be used, otherwise the HTTP protocol will be used. Defaults to true.
    /// </summary>
    public bool UseHttps { get; set; } = true;

    /// <summary>
    /// Determines what version of the API will be used. This should never be modified unless absolutely necessary. Defaults to 2.2.
    /// </summary>
    public string Version { get; set; } = "2.2";

    /// <summary>
    /// Returns the <see cref="ApiUrlBase"/> formatted with the provided parameters.
    /// </summary>
    public string FormattedUrl => ApiUrlBase.Replace("{Protocol}", UseHttps ? "https" : "http").Replace("{Version}", Version);

    /// <summary>
    /// Appends the current <see cref="Key"/> to the provided url.
    /// </summary>
    /// <param name="url">The URL to append to. Should be the result of <see cref="FormattedUrl"/>, then the API </param>
    /// <returns></returns>
    public string AppendKey(string url) => string.IsNullOrWhiteSpace(Key) ? url : url + (url.Contains('?') ? '&' : '?') + "key=" + Key;

    /// <summary>
    /// Returns the fully formatted URL for Stack Exchange API requests.
    /// </summary>
    /// <param name="requester">The fully filled <see cref="IRequest"/> making the request.</param>
    /// <returns>The formatted url.</returns>
    public string GetFormattedUrl<T>(IRequest<T> requester) where T : IBaseModel => AppendKey(FormattedUrl + requester.FormattedEndpoint);
}

You don't actually have to put anything in any of these fields. You can access the API with a default instance of this class as an anonymous user.


Then, as you can see, there is an IRequest<T> interface, which specifies what a request object must contain.

/// <summary>
/// This representes a generic request against the Stack Exchange API. Though this does not make use of the type parameter intrinsically, it's necessary for generic inference and type constraints.
/// </summary>
/// <typeparam name="T">A <see cref="IBaseModel"/> representing the returned model from the request. When used with <see cref="Handler.SubmitRequest{T}(IRequest{T}, bool)"/> this will return a type of <see cref="Wrapper{TObject}"/> where <code>TObject</code> is this type.</typeparam>
public interface IRequest<T>
    where T : IBaseModel
{
    /// <summary>
    /// The basic endpoint for the <see cref="IRequest{T}"/>.
    /// </summary>
    string EndpointUrl { get; }

    /// <summary>
    /// Gets the formatted endpoint for the <see cref="IRequest{T}"/>. This should <b>NOT</b> contain the Stack Exchange API base URL or key.
    /// </summary>
    string FormattedEndpoint { get; }

    /// <summary>
    /// This should verify that all the provided parameters required for the <see cref="IRequest{T}"/> are present.
    /// </summary>
    /// <returns>True if the required parameters pass verification, false otherwise.</returns>
    bool VerifyRequiredParameters();

    /// <summary>
    /// This should return a message to be used to indicate to the user what the verification should be.
    /// </summary>
    string VerificationError { get; }
}

This interface allows us to interact more generically with an API request. It provides a few features we need to guarantee that we are at least attempting to submit a valid API request.


Next we have a Handler class which actually submits and processes the API request.

/// <summary>
/// Fires and processes the actual SE API requests.
/// </summary>
public class Handler
{
    /// <summary>
    /// The <see cref="Configuration"/> to use for general API access.
    /// </summary>
    public Configuration Configuration { get; }

    /// <summary>
    /// Creates a new instance of the <see cref="Handler"/> with the specified <see cref="Configuration"/>.
    /// </summary>
    /// <param name="configuration">The <see cref="Configuration"/> to use for SE API requests.</param>
    public Handler(Configuration configuration)
    {
        Configuration = configuration;
    }

    /// <summary>
    /// Submits a request to the SE API.
    /// </summary>
    /// <typeparam name="T">The type of the object to be returned. This should be inferred from the <see cref="IRequest{T}"/>.</typeparam>
    /// <param name="request">The <see cref="IRequest{T}"/> being performed.</param>
    /// <param name="throwVerificationExceptions">If true, verification errors will result in exceptions. If false, a <code>null</code> object will simply be returned.</param>
    /// <returns>A <see cref="Wrapper{TObject}"/> for the API request.</returns>
    public Wrapper<T> SubmitRequest<T>(IRequest<T> request, bool throwVerificationExceptions = true)
        where T : IBaseModel
    {
        if (!request.VerifyRequiredParameters())
        {
            if (throwVerificationExceptions)
            {
                throw new ArgumentException($"At least one of the required parameters for {nameof(request)} was invalid.", new ArgumentException(request.VerificationError));
            }
            else
            {
                return null;
            }
        }

        var response = "";

        var url = Configuration.GetFormattedUrl(request);

        var webRequest = (HttpWebRequest)WebRequest.Create(url);
        webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
        webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;

        using (var webResponse = webRequest.GetResponse())
        using (var sr = new StreamReader(webResponse.GetResponseStream()))
        {
            response = sr.ReadToEnd();
        }

        return DataContractJsonSerialization.Deserialize<Wrapper<T>>(response);
    }
}

In order for all these type constraints to work, we have a generic IBaseModel which does nothing except provide a type constraint.

/// <summary>
/// Represents a generic model for a Stack Exchange API request. Only required (at the moment) for type constraints.
/// </summary>
public interface IBaseModel
{
}

We have a Wrapper<T> model which represents the base result from an SE API request. All responses from the API (as of now) return this Wrapper with the Items field set to strong-types of the data returned. So, we use a generic implementation so we can provide the strong type ourselves.

/// <summary>
/// This is a general wrapper for Stack Exchange API request models. All API requests should contain these basic fields.
/// </summary>
/// <typeparam name="T">The type of the object list/array returned by the API request.</typeparam>
[DataContract]
public class Wrapper<T> : IBaseModel
    where T : IBaseModel
{
    /// <summary>
    /// A list of the objects returned by the API request.
    /// </summary>
    [DataMember(Name = "items")]
    public List<T> Items { get; set; }

    /// <summary>
    /// Whether or not <see cref="Items"/> returned by this request are the end of the pagination or not.
    /// </summary>
    [DataMember(Name = "has_more")]
    public bool HasMore { get; set; }

    /// <summary>
    /// The maximum number of API requests that can be performed in a 24 hour period.
    /// </summary>
    [DataMember(Name = "quota_max")]
    public int QuotaMax { get; set; }

    /// <summary>
    /// The remaining number of API requests that can be performed in the current 24 hour period.
    /// </summary>
    /// <remarks>
    /// As far as I know, this resets to <see cref="QuotaMax"/> at 00:00:00 UTC+0000.
    /// </remarks>
    [DataMember(Name = "quota_remaining")]
    public int QuotaRemaining { get; set; }

    /// <summary>
    /// The optional number of seconds that the programme making the API requests should stop submitting requests for.
    /// </summary>
    /// <remarks>
    /// Programmes that fail to follow this backoff may be subject to being banned from making API requests for any period of time.
    /// </remarks>
    [DataMember(Name = "backoff")]
    public int? Backoff { get; set; }
}

Then, we have some type of IRequest<T> object which contains the actual data for the API request.

Here are all the currently implemented IRequest<T> models.

/// <summary>
/// Represents an API request for Stack Exchange Site information.
/// </summary>
public class InfoRequest : IRequest<Info>
{
    private const string _endpointUrl = "info?";

    /// <summary>
    /// The destination endpoint for the API request.
    /// </summary>
    public string EndpointUrl => _endpointUrl;

    /// <summary>
    /// The Stack Exchange Site to query the <see cref="Info"/> for.
    /// </summary>
    public string Site { get; set; }

    /// <summary>
    /// The final endpoint URL that should be appended to the Stack Exchange API base url.
    /// </summary>
    public string FormattedEndpoint
    {
        get
        {
            var values = new Dictionary<string, string>();

            values.Add(nameof(Site).ToLower(), Site);

            var qs = StringExtensions.BuildQueryString(values);

            return EndpointUrl + qs;
        }
    }

    /// <summary>
    /// Returns whether or not the <see cref="Site"/> passed verification.
    /// </summary>
    /// <returns>True if <see cref="Site"/> is not a null, empty or whitespace string, false otherwise.</returns>
    public bool VerifyRequiredParameters() => !string.IsNullOrWhiteSpace(Site);

    /// <summary>
    /// Gets a message indicating how <see cref="Site"/> is validated. 
    /// </summary>
    public string VerificationError => $"The value for {nameof(Site)} must be a valid, non-null, and non-whitespace string.";
}

/// <summary>
/// Submits a request to the sites API endpoint, to return a <see cref="Site"/> object.
/// </summary>
/// <remarks>
/// Endpoint URL is <see cref="EndpointUrl"/>.
/// </remarks>
public class SitesRequest : IRequest<Site>
{
    private const string _endpointUrl = "sites?";

    /// <summary>
    /// The destination endpoint for the API request.
    /// </summary>
    public string EndpointUrl => _endpointUrl;

    /// <summary>
    /// Determines how many sites will be returned for each page.
    /// </summary>
    /// <remarks>
    /// As of this writing, this endpoint value is unbounded. Defaults to 1000.
    /// </remarks>
    public int PageSize { get; set; } = 1000;

    public int Page { get; set; } = 1;

    /// <summary>
    /// Returns the fully formatted endpoint for this <see cref="SitesRequest"/> instance.
    /// </summary>

    public string FormattedEndpoint
    {
        get
        {
            var values = new Dictionary<string, string>();

            values.Add(nameof(PageSize).ToLower(), PageSize.ToString());
            values.Add(nameof(Page).ToLower(), Page.ToString());

            var qs = StringExtensions.BuildQueryString(values);

            return EndpointUrl + qs;
        }
    }

    /// <summary>
    /// Verifies that the <see cref="PageSize"/> is a valid value.
    /// </summary>
    /// <returns>True if <see cref="PageSize"/> is greater than 0, false otherwise.</returns>
    public bool VerifyRequiredParameters() => PageSize > 0;

    /// <summary>
    /// Gets a message indicating how <see cref="PageSize"/> is validated.
    /// </summary>
    public string VerificationError => $"The value for {nameof(PageSize)} must be an integer greater than 0.";
}

public class BadgeRequest : IRequest<Badge>
{
    private const string _endpointUrl = "badges?";

    public string EndpointUrl => _endpointUrl;

    public OrderType Order { get; set; }

    public SortType Sort { get; set; }

    public string Site { get; set; }

    public int PageSize { get; set; } = 10;

    public int Page { get; set; } = 1;

    public string Min { get; set; }

    public string Max { get; set; }

    public DateTime? FromDate { get; set; }

    public DateTime? ToDate { get; set; }

    public string FormattedEndpoint
    {
        get
        {
            var values = new Dictionary<string, string>();

            values.Add(nameof(Order).ToLower(), Order == OrderType.Ascending ? "asc" : "desc");
            values.Add(nameof(Sort).ToLower(), Sort.ToString().ToLower());
            values.Add(nameof(Site).ToLower(), Site);
            values.Add(nameof(PageSize).ToLower(), PageSize.ToString());
            values.Add(nameof(Page).ToLower(), Page.ToString());

            if (Min != null)
            {
                values.Add(nameof(Min).ToLower(), Min.ToString());
            }

            if (Min != null)
            {
                values.Add(nameof(Max).ToLower(), Max.ToString());
            }

            if (ToDate != null)
            {
                values.Add(nameof(ToDate).ToLower(), DateTimeExtensions.ToEpoch(ToDate.Value).ToString());
            }

            if (FromDate != null)
            {
                values.Add(nameof(FromDate).ToLower(), DateTimeExtensions.ToEpoch(FromDate.Value).ToString());
            }

            var qs = StringExtensions.BuildQueryString(values);

            return EndpointUrl + qs;
        }
    }

    public string VerificationError => $"The value for {nameof(Site)} must be a valid, non-null, and non-whitespace string; the value for {nameof(PageSize)} must be greater than 0 and less than or equal to {Configuration.MaxPageSize}.";

    public bool VerifyRequiredParameters() => !string.IsNullOrWhiteSpace(Site) && PageSize > 0 && PageSize <= Configuration.MaxPageSize;

    public enum SortType
    {
        Rank,
        Name,
        Type,
    }
}

The OrderType is a simple enum:

public enum OrderType
{
    Ascending,
    Descending,
}

It's in a different file as I may need to use it in other requests.


Finally, there is some type of model that is returned by the API request. Here are all the currently implemented models:

/// <summary>
/// Represents a badge from the Stack Exchange API.
/// </summary>
/// <remarks>
/// http://api.stackexchange.com/docs/types/badge
/// </remarks>
[DataContract]
public class Badge : IBaseModel
{
    /// <summary>
    /// See <code>award_count</code>
    /// </summary>
    [DataMember(Name = "award_count")]
    public int AwardCount { get; set; }

    /// <summary>
    /// See <code>badge_id</code>
    /// </summary>
    [DataMember(Name = "badge_id")]
    public int BadgeId { get; set; }

    /// <summary>
    /// See <code>badge_type</code>
    /// </summary>
    [DataMember(Name = "badge_type")]
    public string BadgeType { get; set; }

    /// <summary>
    /// See <code>link</code>
    /// </summary>
    [DataMember(Name = "link")]
    public string Link { get; set; }

    /// <summary>
    /// See <code>name</code>
    /// </summary>
    [DataMember(Name = "name")]
    public string Name { get; set; }

    /// <summary>
    /// See <code>rank</code>
    /// </summary>
    [DataMember(Name = "rank")]
    public string Rank { get; set; }

    /// <summary>
    /// See <code>user</code>
    /// </summary>
    [DataMember(Name = "user")]
    public ShallowUser User { get; set; }
}

/// <summary>
/// Represents certain statistical data about a Stack Exchange <see cref="Site"/>.
/// </summary>
/// <remarks>
/// http://api.stackexchange.com/docs/types/info
/// </remarks>
[DataContract]
public class Info : IBaseModel
{
    /// <summary>
    /// See <code>answers_per_minute</code>
    /// </summary>
    [DataMember(Name = "answers_per_minute")]
    public decimal AnswersPerMinute { get; set; }

    /// <summary>
    /// See <code>api_revision</code>
    /// </summary>
    [DataMember(Name = "api_revision")]
    public string ApiRevision { get; set; }

    /// <summary>
    /// See <code>badges_per_minute</code>
    /// </summary>
    [DataMember(Name = "badges_per_minute")]
    public decimal BadgesPerMinute { get; set; }

    /// <summary>
    /// See <code>new_active_users</code>
    /// </summary>
    [DataMember(Name = "new_active_users")]
    public int NewActiveUsers { get; set; }

    /// <summary>
    /// See <code>questions_per_minute</code>
    /// </summary>
    [DataMember(Name = "questions_per_minute")]
    public decimal QuestionsPerMinute { get; set; }

    /// <summary>
    /// See <code>total_accepted</code>
    /// </summary>
    [DataMember(Name = "total_accepted")]
    public int TotalAccepted { get; set; }

    /// <summary>
    /// See <code>total_answers</code>
    /// </summary>
    [DataMember(Name = "total_answers")]
    public int TotalAnswers { get; set; }

    /// <summary>
    /// See <code>total_badges</code>
    /// </summary>
    [DataMember(Name = "total_badges")]
    public int TotalBadges { get; set; }

    /// <summary>
    /// See <code>total_comments</code>
    /// </summary>
    [DataMember(Name = "total_comments")]
    public int TotalComments { get; set; }

    /// <summary>
    /// See <code>total_questions</code>
    /// </summary>
    [DataMember(Name = "total_questions")]
    public int TotalQuestions { get; set; }

    /// <summary>
    /// See <code>total_unanswered</code>
    /// </summary>
    [DataMember(Name = "total_unanswered")]
    public int TotalUnanswered { get; set; }

    /// <summary>
    /// See <code>total_users</code>
    /// </summary>
    [DataMember(Name = "total_users")]
    public int TotalUsers { get; set; }

    /// <summary>
    /// See <code>total_votes</code>
    /// </summary>
    [DataMember(Name = "total_votes")]
    public int TotalVotes { get; set; }
}

/// <summary>
/// Represents a site relation to a <see cref="Site"/> in the <see cref="Site.RelatedSites"/> list.
/// </summary>
/// <remarks>
/// http://api.stackexchange.com/docs/types/related-site
/// </remarks>
[DataContract]
public class RelatedSite : IBaseModel
{
    /// <summary>
    /// See <code>api_site_parameter</code>
    /// </summary>
    [DataMember(Name = "api_site_parameter")]
    public string ApiSiteParameter { get; set; }

    /// <summary>
    /// See <code>name</code>
    /// </summary>
    [DataMember(Name = "name")]
    public string Name { get; set; }

    /// <summary>
    /// See <code>relation</code>
    /// </summary>
    [DataMember(Name = "relation")]
    public string Relation { get; set; }

    /// <summary>
    /// See <code>site_url</code>
    /// </summary>
    [DataMember(Name = "site_url")]
    public string SiteUrl { get; set; }
}

/// <summary>
/// Represents a partial user on the Stack Exchange API.
/// </summary>
/// <remarks>
/// http://api.stackexchange.com/docs/types/shallow-user
/// </remarks>
[DataContract]
public class ShallowUser
{
    /// <summary>
    /// See <code>accept_rate</code>
    /// </summary>
    [DataMember(Name = "accept_rate")]
    public int? AcceptRate { get; set; }

    /// <summary>
    /// See <code>display_name</code>
    /// </summary>
    [DataMember(Name = "display_name")]
    public string DisplayName { get; set; }

    /// <summary>
    /// See <code>link</code>
    /// </summary>
    [DataMember(Name = "link")]
    public string Link { get; set; }

    /// <summary>
    /// See <code>profile_image</code>
    /// </summary>
    [DataMember(Name = "profile_image")]
    public string ProfileImage { get; set; }

    /// <summary>
    /// See <code>reputation</code>
    /// </summary>
    [DataMember(Name = "reputation")]
    public int? Reputation { get; set; }

    /// <summary>
    /// See <code>user_id</code>
    /// </summary>
    [DataMember(Name = "user_id")]
    public int? UserId { get; set; }

    /// <summary>
    /// See <code>user_type</code>
    /// </summary>
    [DataMember(Name = "user_type")]
    public string UserType { get; set; }
}

/// <summary>
/// Represents a Stack Exchange site.
/// </summary>
/// <remarks>
/// http://api.stackexchange.com/docs/types/site
/// </remarks>
[DataContract]
public class Site : IBaseModel
{
    /// <summary>
    /// See <code>aliases</code>
    /// </summary>
    [DataMember(Name = "aliases")]
    public List<string> Aliases { get; set; }

    /// <summary>
    /// See <code>api_site_parameter</code>
    /// </summary>
    [DataMember(Name = "api_site_parameter")]
    public string ApiSiteParameter { get; set; }

    /// <summary>
    /// See <code>audience</code>
    /// </summary>
    [DataMember(Name = "audience")]
    public string Audience { get; set; }

    /// <summary>
    /// See <code>closed_beta_date</code>
    /// </summary>
    [DataMember(Name = "closed_beta_date")]
    public long? ClosedBetaDate { get; set; }

    /// <summary>
    /// A .NET DateTime? representing the <see cref="ClosedBetaDate"/>.
    /// </summary>
    public DateTime? ClosedBetaDateTime { get { return DateTimeExtensions.FromEpoch(ClosedBetaDate); } set { ClosedBetaDate = DateTimeExtensions.ToEpoch(value); } }

    /// <summary>
    /// See <code>favicon_url</code>
    /// </summary>
    [DataMember(Name = "favicon_url")]
    public string FaviconUrl { get; set; }

    /// <summary>
    /// See <code>high_resolution_icon_url</code>
    /// </summary>
    [DataMember(Name = "high_resolution_icon_url")]
    public string HighResolutionIconUrl { get; set; }

    /// <summary>
    /// See <code>icon_url</code>
    /// </summary>
    [DataMember(Name = "icon_url")]
    public string IconUrl { get; set; }

    /// <summary>
    /// See <code>launch_date</code>
    /// </summary>
    [DataMember(Name = "launch_date")]
    public long LaunchDate { get; set; }

    /// <summary>
    /// A .NET DateTime representing the <see cref="LaunchDate"/>.
    /// </summary>
    public DateTime LaunchDateTime { get { return DateTimeExtensions.FromEpoch(LaunchDate); } set { LaunchDate = DateTimeExtensions.ToEpoch(value); } }

    /// <summary>
    /// See <code>logo_url</code>
    /// </summary>
    [DataMember(Name = "logo_url")]
    public string LogoUrl { get; set; }

    /// <summary>
    /// See <code>markdown_extensions</code>
    /// </summary>
    [DataMember(Name = "markdown_extensions")]
    public List<string> MarkdownExtensions { get; set; }

    /// <summary>
    /// See <code>name</code>
    /// </summary>
    [DataMember(Name = "name")]
    public string Name { get; set; }

    /// <summary>
    /// See <code>open_beta_date</code>
    /// </summary>
    [DataMember(Name = "open_beta_date")]
    public long? OpenBetaDate { get; set; }

    /// <summary>
    /// A .NET DateTime? representing the <see cref="OpenBetaDate"/>.
    /// </summary>
    public DateTime? OpenBetaDateTime { get { return DateTimeExtensions.FromEpoch(OpenBetaDate); } set { OpenBetaDate = DateTimeExtensions.ToEpoch(value); } }

    /// <summary>
    /// See <code>related_sites</code>
    /// </summary>
    [DataMember(Name = "related_sites")]
    public List<RelatedSite> RelatedSites { get; set; }

    /// <summary>
    /// See <code>site_state</code>
    /// </summary>
    [DataMember(Name = "site_state")]
    public string SiteState { get; set; }

    /// <summary>
    /// See <code>site_type</code>
    /// </summary>
    [DataMember(Name = "site_type")]
    public string SiteType { get; set; }

    /// <summary>
    /// See <code>site_url</code>
    /// </summary>
    [DataMember(Name = "site_url")]
    public string SiteUrl { get; set; }

    /// <summary>
    /// See <code>styling</code>
    /// </summary>
    [DataMember(Name = "styling")]
    public Styling Styling { get; set; }

    /// <summary>
    /// See <code>twitter_account</code>
    /// </summary>
    [DataMember(Name = "twitter_account")]
    public string TwitterAccount { get; set; }
}

/// <summary>
/// Represents the <see cref="Site.Styling"/>.
/// </summary>
/// <remarks>
/// http://api.stackexchange.com/docs/types/styling
/// </remarks>
[DataContract]
public class Styling : IBaseModel
{
    /// <summary>
    /// See <code>link_color</code>
    /// </summary>
    [DataMember(Name = "link_color")]
    public string LinkColor { get; set; }

    /// <summary>
    /// See <code>tag_background_color</code>
    /// </summary>
    [DataMember(Name = "tag_background_color")]
    public string TagBackgroundColor { get; set; }

    /// <summary>
    /// See <code>tag_foreground_color</code>
    /// </summary>
    [DataMember(Name = "tag_foreground_color")]
    public string TagForegroundColor { get; set; }
}

Using the API is super simple, which is how I wanted it. It's pretty much four lines of code (if you don't do inline construction) to send an anonymous request:

var config = new Configuration();
var handler = new Handler(config);
var request = new SitesRequest();
var sites = handler.SubmitRequest(request);

To send a non-anonymous request with an application key, simply put the key in the config object:

var config = new Configuration();
config.Key = "Some SE API Key.";
var handler = new Handler(config);
var request = new SitesRequest();
var sites = handler.SubmitRequest(request);

You can also compress things into fewer lines pretty easily.

var sites = new Handler(new Configuration { Key = "Some SE API Key." }).SubmitRequest(new SitesRequest());

Feel free to comment on anything and everything. I want to make sure I'm doing this in an understandable, modular and powerful way before I proceed.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Overall I like your code. It is understandable, (almost) easy to read and well structured. But I have some small pointers I would like to address.

  • In the Handler.SubmitRequest<T>() method you have

    var url = Configuration.GetFormattedUrl(request);  
    

    where I would preffer using the explicit type so one just looking at your code doesn't need to check that GetFormattedUrl() returns a string.

    Another thing is the name of that method because it isn't only submitting a request but is processing the response as well. Maybe ProcessRequest would be a better name.

  • If one can use the Configuration class without setting any property he/she should have the possibility to use the Handler class without having to pass a Configuration to its constructor. You could add an parameterless constructor which passes a new Configuration() to the current constructor.

  • The FromattedEnpointUrl property of the InfoRequest looks odd to me. Whats the need for a dictionary if it contains only one key and value ? Why not let the StringExtensions.BuildQueryString() method have 2 string parameters ?

  • Some of the properties which aren't autoimplemented ones are hard to read because you placed both the getter and setter on the same line. This leads to the need to scroll to the right which is removing readability. Like this

    public DateTime? OpenBetaDateTime { get { return DateTimeExtensions.FromEpoch(OpenBetaDate); } set { OpenBetaDate = DateTimeExtensions.ToEpoch(value); } }  
    

    IMO this would be better like so

    public DateTime? OpenBetaDateTime 
    { 
        get { return DateTimeExtensions.FromEpoch(OpenBetaDate); } 
        set { OpenBetaDate = DateTimeExtensions.ToEpoch(value); } 
    }  
    

    Btw, I hope the DateTimeExtensions.ToEpoch() method can handle nullable's well.

  • I don't like the AppendKey() method of the Configuration class either. Having 2 ternary expressions in a oneliner is IMO too much.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ As far as "Btw, I hope the DateTimeExtensions.ToEpoch() method can handle nullable's well.", it can. :) \$\endgroup\$ Commented Apr 12, 2016 at 8:52
  • \$\begingroup\$ When using a default configuration rather than passing new Configuration all the time create a static field on Configuration called Default and create an instance there then reuse it. Then you could change your defaults... or even if u didn't want that (make it internal) at least your not creating many configuration instances when only 1 is needed \$\endgroup\$
    – Sam
    Commented Apr 14, 2016 at 3:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.