<repo_name>MarkDaviesEsendex/BattleNetClient<file_sep>/src/BattleNetClient/Clients/BattleNetClient.cs
﻿using BattleNetClient.Diablo;

namespace BattleNetClient.Clients
{
    internal class BattleNetClient : IBattleNetClient
    {
        public IDiabloClient Diablo { get; }

        public bool IsClientValid => true;

        public BattleNetClient(IDiabloClient diabloClient)
        {
            Diablo = diabloClient;
        }
    }
}
<file_sep>/src/BattleNetClient.Testing/Data/Models/Token.cs
﻿using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BattleNetClient.Testing.Data.Models
{
    public class Token
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int  Id { get; set; }
        public string AccessToken { get; set; }
    }
}<file_sep>/src/BattleNetClient.Testing/Models/Diablo/Act.cs
using System.Collections.Generic;

namespace BattleNetClient.Testing.Models.Diablo
{
    public class Act
    {
        public string Slug { get; set; }
        public int Number { get; set; }
        public string Name { get; set; }

        public List<Quest> Quests { get; set; } = new List<Quest>();
    }
}<file_sep>/src/BattleNetClient.Testing/BattleNetServer.cs
﻿using System.IO;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;

namespace BattleNetClient.Testing
{
    public interface IBattleNetServer
    {
        HttpClient GetClient();
        string[] GetRequests();
    }

    internal class BattleNetServer : IBattleNetServer
    {
        private readonly TestServer _server;
        private readonly HttpClient _client;

        public BattleNetServer()
        {
            _server = new TestServer(new WebHostBuilder()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseStartup<Startup>());
            _client = _server.CreateClient();
        }

        public HttpClient GetClient() 
            => _client;

        public string[] GetRequests() 
            => new string[0];
    }
}
<file_sep>/src/BattleNetClient/Diablo/NullActClient.cs
﻿using System.Net;
using System.Threading.Tasks;
using BattleNetClient.Diablo.Models;

namespace BattleNetClient.Diablo
{
    public class NullActClient : IActClient
    {
        public Task<IResult<Act>> GetByIdAsync(int actId) 
            => Task.FromResult(Result<Act>.Failure(Act.Empty, HttpStatusCode.NotFound));

        public Task<IResult<ActCollection>> GetAllAsync()
            => Task.FromResult(Result<ActCollection>.Failure(ActCollection.Empty, HttpStatusCode.NotFound));
    }
}<file_sep>/src/BattleNetClient/Diablo/NullDiabloClient.cs
﻿namespace BattleNetClient.Diablo
{
    internal class NullDiabloClient : IDiabloClient
    {
        public NullDiabloClient() 
            => Act = new NullActClient();

        public IActClient Act { get; }
    }
}<file_sep>/README.md
# Battle Net Client
This is a c# library that will allow you to interact with the blizzard apis, in a clean testable way - that's the hope anyway.

# Usage
## Create a client:
```
var factory = new BattleNetClientFactory(Region.UnitedStates);
var client = await _factory.CreateClientAsync("clientId", "clientSecret");
```

*Note*: to aquire a client id and a client seecret please go and see the documentation for the blizzard api: https://develop.battle.net/, I am not in charge of this.

## Check validity
If the api pases back an invalid status code (anything not in the 200 range) the sdk will pass back an invalid client, to check this please use the `IsClientValid` property, the client will allow you to interact with it (specified later in the readme) but will _always_ return invalid results.

Whilst people may disagree with this approach, I always feel that a _good_ library will always return objects that the consuming code can interact with.

## Query the api:
```
var factory = new BattleNetClientFactory(Region.UnitedStates);
var client = await factory.CreateClientAsync("clientId", "clientSecret");

if(client.IsValid)
{
    // Get act by id
    var actualActResult = await client.Diablo.Act.GetByIdAsync(1);
}
```

This will pass back a result object:
```
public interface IResult<out T>
{
    bool IsValid { get; }
    T Data { get; }
    HttpStatusCode Code { get; }
}
```

## Check validaity
Before using the object check that it is valid:
```
if(actualActResult.IsValid)
{
    // do your thing --- make your body sing
}
```
*Note*: Again if you have an invalid version of the object it will _not_ be null, it will be a real obejct that you can interact with it will just have no value in the properties i.e. string will be empty, ints will be -1.

# Testing
I have split the project into three libraries:
* Client
* Client Tests
* Client Testing

My hope is that I will post both the client and the client testing libraries onto nuget, and you can simply take advantage of the code I've used to test my code, for example, to test acts:
```
// Create fake server
var server = new BattleNetServerBuilder()
    .CreateClientCredentials("clientId", "clientSecret")
    .Build();

// create client with fake server client
_factory = new BattleNetClientFactory(Region.UnitedStates, server.GetClient());

```

Maybe adding configurable data sometime down the line.<file_sep>/src/BattleNetClient.Testing/Models/GrantToken.cs
﻿using Newtonsoft.Json;

namespace BattleNetClient.Testing.Models
{
    public class GrantToken
    {
        [JsonProperty("grant_type")]
        public string GrantType { get; set; }
    }
}<file_sep>/src/BattleNetClient/Diablo/Models/ActCollection.cs
using System.Collections.Generic;

namespace BattleNetClient.Diablo.Models
{
    public class ActCollection
    {
        public static ActCollection Empty => new NullActCollection();
        public virtual List<Act> Acts { get; } = new List<Act>();
    }
}<file_sep>/src/BattleNetClient/Clients/IBattleNetClient.cs
﻿using BattleNetClient.Diablo;

namespace BattleNetClient.Clients
{
    public interface IBattleNetClient
    {
        IDiabloClient Diablo { get; }
        
        bool IsClientValid { get; }
    }
}<file_sep>/src/BattleNetClient.Testing/Data/AuthenticationDbContext.cs
﻿using BattleNetClient.Testing.Data.Models;
using Microsoft.EntityFrameworkCore;

namespace BattleNetClient.Testing.Data
{
    public class AuthenticationDbContext : DbContext
    {
        public DbSet<ClientInfo> ClientInfo { get; set; }
        public DbSet<Token> Tokens { get; set; }

        public AuthenticationDbContext(DbContextOptions options)
            : base(options)
        {
        }
    }
}<file_sep>/src/BattleNetClient.Testing/BasicAuthorization.cs
﻿namespace BattleNetClient.Testing
{
    public class BasicAuthorization
    {
        public string ClientId { get; private set; }
        public string ClientSecret { get; private set; }

        public static BasicAuthorization Parse(string authentication)
        {
            var base64String = authentication.Split(' ')[1];
            var token = Base64Decode(base64String);
            var clientInfo = token.Split(":");
            return clientInfo.Length != 2
                ? null
                : new BasicAuthorization {ClientId = clientInfo[0], ClientSecret = clientInfo[1]};
        }

        private static string Base64Decode(string base64EncodedData)
        {
            var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);
            return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
        }
    }
}<file_sep>/src/BattleNetClient/Diablo/Models/Act.cs
﻿using System.Collections.Generic;

namespace BattleNetClient.Diablo.Models
{
    public class Act
    {
        public static Act Empty => new NullAct();
        
        public virtual string Slug { get; }
        public virtual int Number { get; }
        public virtual string Name { get; }

        public virtual List<Quest> Quests { get; } = new List<Quest>();
    }
}<file_sep>/src/BattleNetClient/Diablo/ActClient.cs
﻿using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using BattleNetClient.Clients;
using BattleNetClient.Diablo.Models;

namespace BattleNetClient.Diablo
{
    public interface IActClient
    {
        Task<IResult<Act>> GetByIdAsync(int actId);
        Task<IResult<ActCollection>> GetAllAsync();
    }
    
    public class ActClient : IActClient
    {
        private readonly ClientHelper _clientHelper;

        public ActClient(ClientHelper clientHelper)
        {
            _clientHelper = clientHelper;
        }

        public async Task<IResult<Act>> GetByIdAsync(int actId)
        {
            var responseMessage =
                await _clientHelper.Client.GetAsync($"https://us.api.blizzard.com/d3/data/act/{actId}");
            return !responseMessage.IsSuccessStatusCode
                ? Result<Act>.Failure(Act.Empty, responseMessage.StatusCode)
                : Result<Act>.Success(await responseMessage.Content.ReadAsAsync<Act>());
        }

        public async Task<IResult<ActCollection>> GetAllAsync()
        {
            var responseMessage =
                await _clientHelper.Client.GetAsync($"https://us.api.blizzard.com/d3/data/act/");
            return !responseMessage.IsSuccessStatusCode
                ? Result<ActCollection>.Failure(ActCollection.Empty, responseMessage.StatusCode)
                : Result<ActCollection>.Success(await responseMessage.Content.ReadAsAsync<ActCollection>());
        }
    }
}<file_sep>/src/BattleNetClient.Testing/Models/Diablo/ActCollection.cs
using System.Collections.Generic;

namespace BattleNetClient.Testing.Models.Diablo
{
    public class ActCollection
    {
        public virtual List<Act> Acts { get; } = new List<Act>();
    }
}<file_sep>/src/BattleNetClient.Testing/Data/Models/ClientInfo.cs
﻿using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BattleNetClient.Testing.Data.Models
{
    public class ClientInfo
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string ClientId { get; set; }
        public string ClientSecret { get; set; }
    }
}<file_sep>/src/BattleNetClient.Testing/Data/AuthRepository.cs
﻿using System;
using System.Linq;
using BattleNetClient.Testing.Data.Models;

namespace BattleNetClient.Testing.Data
{
    public interface IRepository<T> where T : class
    {
        T InsertRecord(T record);
        T GetRecord(Func<T, bool> whereStatement);
    }

    public class AuthRepository<T> : IRepository<T> where T : class
    {
        private readonly AuthenticationDbContext _context;

        public AuthRepository(AuthenticationDbContext context)
        {
            _context = context;
        }

        public T InsertRecord(T record)
        {
            try
            {
                _context.Set<T>().Add(record);
                _context.SaveChanges();
                return record;
            }
            catch (Exception)
            {
                return default(T);
            }
        }

        public T GetRecord(Func<T, bool> whereStatement)
        {
            return _context.Set<T>().First(whereStatement);
        }
    }
}<file_sep>/src/BattleNetClient/Clients/ClientHelper.cs
﻿using System.Net.Http;

namespace BattleNetClient.Clients
{
    public class ClientHelper
    {
        public ClientHelper(HttpClient client, BattleNetRegion battleNetRegion)
        {
            Client = client;
            BattleNetRegion = battleNetRegion;
        }

        public HttpClient Client { get; }
        public BattleNetRegion BattleNetRegion { get; }
    }
}<file_sep>/src/BattleNetClient/Clients/NullClient.cs
﻿using BattleNetClient.Diablo;

namespace BattleNetClient.Clients
{
    internal class NullClient : IBattleNetClient
    {
        public NullClient() 
            => Diablo = new NullDiabloClient();

        public IDiabloClient Diablo { get; }
        public bool IsClientValid => false;
    }
}<file_sep>/src/BattleNetClient.Tests/Diablo/Acts/GetActById.cs
﻿using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using BattleNetClient.Diablo.Models;
using BattleNetClient.Testing;
using Newtonsoft.Json;
using Xunit;

namespace BattleNetClient.Tests.Diablo.Acts
{
    public class GetActById
    {
        private readonly BattleNetClientFactory _factory;

        public GetActById()
        {
            var server = new BattleNetServerBuilder()
                .CreateClientCredentials("clientId", "clientSecret")
                .Build();

            _factory = new BattleNetClientFactory(BattleNetRegion.UnitedStates, server.GetClient());
        }

        [Theory]
        [MemberData(nameof(TestData))]
        public async Task WhenValidActIsRequested_ThenExpectedActIsReturned(int actId, string actString)
        {
            var client = await _factory.CreateClientAsync("clientId", "clientSecret");
            var actualActResult = await client.Diablo.Act.GetByIdAsync(actId);
            var expectedAct = JsonConvert.DeserializeObject<Act>(actString);

            var actualAct = actualActResult.Data;
            Assert.True(actualActResult.IsValid);
            Assert.Equal(HttpStatusCode.OK, actualActResult.Code);
            Assert.Equal(expectedAct.Slug, actualAct.Slug);
            Assert.Equal(expectedAct.Number, actualAct.Number);
            Assert.Equal(expectedAct.Name, actualAct.Name);

            for (var i = 0; i < expectedAct.Quests.Count; i++)
            {
                Assert.Equal(expectedAct.Quests[i].Id, actualAct.Quests[i].Id);
                Assert.Equal(expectedAct.Quests[i].Name, actualAct.Quests[i].Name);
                Assert.Equal(expectedAct.Quests[i].Slug, actualAct.Quests[i].Slug);
            }
        }
        
        [Fact]
        public async Task WhenInvalidActIsRequested_ThenNoActIsReturned()
        {
            var client = await _factory.CreateClientAsync("clientId", "clientSecret");
            var actualActResult = await client.Diablo.Act.GetByIdAsync(52);
            var actualAct = actualActResult.Data;

            Assert.False(actualActResult.IsValid);
            Assert.Equal(HttpStatusCode.NotFound, actualActResult.Code);
            Assert.Empty(actualAct.Slug);
            Assert.Equal(-1, actualAct.Number);
            Assert.Empty(actualAct.Name);
            Assert.Empty(actualAct.Quests);
        }

        public static IEnumerable<object[]> TestData => Data;
        private static readonly List<object[]> Data
            = new List<object[]>
            {
                new object[] {1, "{\"slug\":\"act-i\",\"number\":1,\"name\":\"Act I\",\"quests\":[{\"id\":87700,\"name\":\"The Fallen Star\",\"slug\":\"the-fallen-star\"},{\"id\":72095,\"name\":\"The Legacy of Cain\",\"slug\":\"the-legacy-of-cain\"},{\"id\":72221,\"name\":\"A Shattered Crown\",\"slug\":\"a-shattered-crown\"},{\"id\":72061,\"name\":\"Reign of the Black King\",\"slug\":\"reign-of-the-black-king\"},{\"id\":117779,\"name\":\"Sword of the Stranger\",\"slug\":\"sword-of-the-stranger\"},{\"id\":72738,\"name\":\"The Broken Blade\",\"slug\":\"the-broken-blade\"},{\"id\":73236,\"name\":\"The Doom in Wortham\",\"slug\":\"the-doom-in-wortham\"},{\"id\":72546,\"name\":\"Trailing the Coven\",\"slug\":\"trailing-the-coven\"},{\"id\":72801,\"name\":\"The Imprisoned Angel\",\"slug\":\"the-imprisoned-angel\"},{\"id\":136656,\"name\":\"Return to New Tristram\",\"slug\":\"return-to-new-tristram\"}]}"},
                new object[] {2, "{\"slug\":\"act-ii\",\"number\":2,\"name\":\"Act II\",\"quests\":[{\"id\":80322,\"name\":\"Shadows in the Desert\",\"slug\":\"shadows-in-the-desert\"},{\"id\":93396,\"name\":\"The Road to Alcarnus\",\"slug\":\"the-road-to-alcarnus\"},{\"id\":74128,\"name\":\"City of Blood\",\"slug\":\"city-of-blood\"},{\"id\":57331,\"name\":\"A Royal Audience\",\"slug\":\"a-royal-audience\"},{\"id\":78264,\"name\":\"Unexpected Allies\",\"slug\":\"unexpected-allies\"},{\"id\":78266,\"name\":\"Betrayer of the Horadrim\",\"slug\":\"betrayer-of-the-horadrim\"},{\"id\":57335,\"name\":\"Blood and Sand\",\"slug\":\"blood-and-sand\"},{\"id\":57337,\"name\":\"The Black Soulstone\",\"slug\":\"the-black-soulstone\"},{\"id\":121792,\"name\":\"The Scouring of Caldeum\",\"slug\":\"the-scouring-of-caldeum\"},{\"id\":57339,\"name\":\"Lord of Lies\",\"slug\":\"lord-of-lies\"}]}"},
            };
    }
}<file_sep>/src/BattleNetClient.Testing/Models/TokenResponse.cs
﻿using Newtonsoft.Json;

namespace BattleNetClient.Testing.Models
{
    public class TokenResponse
    {
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }
    }
}<file_sep>/src/BattleNetClient/BattleNetRegion.cs
﻿namespace BattleNetClient
{
    public enum BattleNetRegion
    {
        UnitedStates
    }
}<file_sep>/src/BattleNetClient.Testing/Controllers/AuthenticationController.cs
﻿using System;
using System.Collections.Generic;
using BattleNetClient.Testing.Data;
using BattleNetClient.Testing.Data.Models;
using BattleNetClient.Testing.Models;
using Microsoft.AspNetCore.Mvc;

namespace BattleNetClient.Testing.Controllers
{
    [Route("oauth")]
    public class AuthenticationController : ControllerBase
    {
        private readonly IRepository<ClientInfo> _clientInfoRepository;
        private readonly IRepository<Token> _tokenRepository;

        public AuthenticationController(IRepository<ClientInfo> clientInfoRepository, IRepository<Token> tokenRepository)
        {
            _clientInfoRepository = clientInfoRepository;
            _tokenRepository = tokenRepository;
        }

        [HttpPost("token")]
        public ActionResult<TokenResponse> GenerateToken([FromHeader(Name = "Authorization")]string authentication, [FromForm]List<KeyValuePair<string, string>> content)
        {
            if (!authentication.StartsWith("Basic "))
                return NotFound();

            var authorization = BasicAuthorization.Parse(authentication);
            if (authorization == null)
                return NotFound();

            var clientInfoFromDb = _clientInfoRepository.GetRecord(info =>
                info.ClientId == authorization.ClientId && info.ClientSecret == authorization.ClientSecret);

            if (clientInfoFromDb == null)
                return NotFound();

            var token = _tokenRepository.InsertRecord(new Token { AccessToken = Guid.NewGuid().ToString()});
            return Ok(new TokenResponse { AccessToken = token.AccessToken});
        }

        [HttpPost("clientInfo")]
        public void CreateClientInformation([FromBody]ClientInfoRequest infoRequest)
            => _clientInfoRepository.InsertRecord(new ClientInfo {ClientId = infoRequest.ClientId, ClientSecret = infoRequest.ClientSecret});
    }
}<file_sep>/src/BattleNetClient.Testing/BattleNetServerBuilder.cs
﻿using System.Net.Http;
using System.Net.Http.Formatting;
using System.Threading.Tasks;

namespace BattleNetClient.Testing
{
    public class BattleNetServerBuilder
    {
        private readonly IBattleNetServer _server;

        public BattleNetServerBuilder() 
            => _server = new BattleNetServer();

        public BattleNetServerBuilder CreateClientCredentials(string clientId, string clientSecret)
        {
            Task.Run(async () =>
            {
                await _server.GetClient().PostAsync("/oauth/clientInfo", new {clientId, clientSecret},
                    new JsonMediaTypeFormatter());
            }).Wait();
            return this;
        }

        public IBattleNetServer Build() 
            => _server;
    }
}<file_sep>/src/BattleNetClient/Diablo/DiabloClient.cs
﻿namespace BattleNetClient.Diablo
{
    public interface IDiabloClient
    {
        IActClient Act { get; }
    }
    
    internal class DiabloClient : IDiabloClient
    {
        public IActClient Act { get; }

        public DiabloClient(IActClient actClient)
        {
            Act = actClient;
        }
    }
}
<file_sep>/src/BattleNetClient/Diablo/Models/NullActCollection.cs
using System.Collections.Generic;

namespace BattleNetClient.Diablo.Models
{
    public class NullActCollection : ActCollection
    {
        public override List<Act> Acts => new List<Act>();
    }
}<file_sep>/src/BattleNetClient/BattleNetClientFactory.cs
﻿using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using BattleNetClient.Clients;
using BattleNetClient.Diablo;

namespace BattleNetClient
{
    public class BattleNetClientFactory
    {
        private readonly BattleNetRegion _battleNetRegion;
        private readonly HttpClient _client;

        public BattleNetClientFactory(BattleNetRegion battleNetRegion, HttpClient client = null)
        {
            _battleNetRegion = battleNetRegion;
            _client = client ?? new HttpClient();
        }

        public async Task<IBattleNetClient> CreateClientAsync(string clientId, string clientSecret)
        {
            var response = await MakeAuthenticationRequest(clientId, clientSecret);
            if (!response.IsSuccessStatusCode)
                return new NullClient();

            var tokenResponse = await response.Content.ReadAsAsync<TokenResponse>();
            _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
            return CreateClient();
        }

        private async Task<HttpResponseMessage> MakeAuthenticationRequest(string clientId, string clientSecret)
        {
            var authenticationHeaderValue = CreateAuthenticationHeader(clientId, clientSecret);
            _client.DefaultRequestHeaders.Authorization = authenticationHeaderValue;
            return await _client.PostAsync("https://us.battle.net/oauth/token", new FormUrlEncodedContent(
                new List<KeyValuePair<string, string>>
                    {new KeyValuePair<string, string>("grant_type", "client_credentials")}));
        }

        private Clients.BattleNetClient CreateClient()
        {
            var clientHelper = new ClientHelper(_client, _battleNetRegion);
            var actClient = new ActClient(clientHelper);
            var diabloClient = new DiabloClient(actClient);
            return new Clients.BattleNetClient(diabloClient);
        }

        private static AuthenticationHeaderValue CreateAuthenticationHeader(string clientId, string clientSecret) => new AuthenticationHeaderValue("Basic",
            Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}")));
    }
}<file_sep>/src/BattleNetClient.Tests/ClientFactoryTests/CreateClientTests.cs
﻿using System.Threading.Tasks;
using BattleNetClient.Testing;
using Xunit;

namespace BattleNetClient.Tests.ClientFactoryTests
{
    public class CreateClientTests
    {
        private readonly BattleNetClientFactory _factory;

        public CreateClientTests()
        {
            var server = new BattleNetServerBuilder()
                .CreateClientCredentials("clientId", "clientSecret")
                .Build();

            _factory = new BattleNetClientFactory(BattleNetRegion.UnitedStates, server.GetClient());
        }

        [Fact]
        public async Task WhenClientIsCreatedWithIncorrectCredential_ThenClientIsNonAuthenticated()
        {
            var client = await _factory.CreateClientAsync("", "");
            Assert.False(client.IsClientValid);
        }

        [Fact]
        public async Task WhenClientIsCreated_ThenValidClientIsReturned()
        {
            var battleNetClient = await _factory.CreateClientAsync("clientId", "clientSecret");
            Assert.True(battleNetClient.IsClientValid);
        }
    }
}<file_sep>/src/BattleNetClient.Testing/Startup.cs
﻿using BattleNetClient.Testing.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BattleNetClient.Testing
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<AuthenticationDbContext>(op => op.UseInMemoryDatabase("Auth"));
            services.AddTransient<AuthenticationDbContext>();
            services.AddTransient(typeof(IRepository<>), typeof(AuthRepository<>));
            services.AddRouting();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseMvc();
        }
    }
}
<file_sep>/src/BattleNetClient/Result.cs
using System.Net;
using BattleNetClient.Diablo.Models;

namespace BattleNetClient
{
    public interface IResult<out T>
    {
        bool IsValid { get; }
        T Data { get; }
        HttpStatusCode Code { get; }
    }

    public class Result<T> : IResult<T>
    {
        private Result(bool isValid, T data, HttpStatusCode code)
        {
            IsValid = isValid;
            Data = data;
            Code = code;
        }
        
        public bool IsValid { get; }
        public T Data { get; }
        public HttpStatusCode Code { get; }
        
        public static IResult<T> Success(T data) 
            => new Result<T>(true, data, HttpStatusCode.OK);

        public static IResult<T> Failure(T data, HttpStatusCode statusCode) 
            => new Result<T>(false, data, statusCode);
    }
}<file_sep>/src/BattleNetClient/Diablo/Models/NullAct.cs
﻿using System.Collections.Generic;

namespace BattleNetClient.Diablo.Models
{
    public class NullAct : Act
    {
        public override string Slug => "";
        public override int Number => -1;
        public override string Name => "";

        public override List<Quest> Quests => new List<Quest>();
    }
}