diff --git a/.editorconfig b/.editorconfig index 2d03a88..1b25fe4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -126,6 +126,8 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true +[*.{cs,vb}] +dotnet_diagnostic.IDE0290.severity = none ############################### # VB Coding Conventions # ############################### diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 454c2ae..ff88a16 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build PokéData Solution +name: Build PokéData Solution on: push: @@ -8,11 +8,11 @@ on: jobs: build: - name: Build PokéData Solution + name: Build PokéData Solution runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Build Docker Image - run: docker build . -t francispion.azurecr.io/pokedata:${{ github.sha }} -f src/PokeData.ETL/Dockerfile + run: docker build . -t francispion.azurecr.io/pokedata:${{ github.sha }} -f src/PokeData/Dockerfile diff --git a/.gitignore b/.gitignore index 7a9bff2..bd68be6 100644 --- a/.gitignore +++ b/.gitignore @@ -361,8 +361,3 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd - -# Output files -resources.json -species.csv -species.json diff --git a/PokeData.sln b/PokeData.sln index 1a266f8..e36f6f7 100644 --- a/PokeData.sln +++ b/PokeData.sln @@ -10,11 +10,26 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitattributes = .gitattributes .gitignore = .gitignore CHANGELOG.md = CHANGELOG.md + docker-compose.yml = docker-compose.yml LICENSE = LICENSE README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.ETL", "src\PokeData.ETL\PokeData.ETL.csproj", "{FC44EF38-D8A6-4A84-84AB-7597501F54BF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokeData.ETL", "src\PokeData.ETL\PokeData.ETL.csproj", "{FC44EF38-D8A6-4A84-84AB-7597501F54BF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.EntityFrameworkCore.Relational", "src\PokeData.EntityFrameworkCore.Relational\PokeData.EntityFrameworkCore.Relational.csproj", "{2F7CBF71-7E37-4539-A193-348BFC76AB06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.Domain", "src\PokeData.Domain\PokeData.Domain.csproj", "{ABD45B7D-7392-4829-B18B-20E4472F76F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.Application", "src\PokeData.Application\PokeData.Application.csproj", "{A01D90FC-6A0B-4B81-B37F-8CA9F7C3EDC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.Infrastructure", "src\PokeData.Infrastructure\PokeData.Infrastructure.csproj", "{F3366415-E934-4B9B-BAA5-9E6327DAD65D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.Infrastructure.PokeApiClient", "src\PokeData.Infrastructure.PokeApiClient\PokeData.Infrastructure.PokeApiClient.csproj", "{9D563A01-E981-47AB-9DC8-64E65BDE8949}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.EntityFrameworkCore.SqlServer", "src\PokeData.EntityFrameworkCore.SqlServer\PokeData.EntityFrameworkCore.SqlServer.csproj", "{A97D552B-387E-4BB9-9D56-B6E1D950DE5B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData", "src\PokeData\PokeData.csproj", "{1448F506-73E9-4664-A419-002B79A8C276}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,6 +41,34 @@ Global {FC44EF38-D8A6-4A84-84AB-7597501F54BF}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC44EF38-D8A6-4A84-84AB-7597501F54BF}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC44EF38-D8A6-4A84-84AB-7597501F54BF}.Release|Any CPU.Build.0 = Release|Any CPU + {2F7CBF71-7E37-4539-A193-348BFC76AB06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F7CBF71-7E37-4539-A193-348BFC76AB06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F7CBF71-7E37-4539-A193-348BFC76AB06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F7CBF71-7E37-4539-A193-348BFC76AB06}.Release|Any CPU.Build.0 = Release|Any CPU + {ABD45B7D-7392-4829-B18B-20E4472F76F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABD45B7D-7392-4829-B18B-20E4472F76F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABD45B7D-7392-4829-B18B-20E4472F76F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABD45B7D-7392-4829-B18B-20E4472F76F2}.Release|Any CPU.Build.0 = Release|Any CPU + {A01D90FC-6A0B-4B81-B37F-8CA9F7C3EDC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A01D90FC-6A0B-4B81-B37F-8CA9F7C3EDC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A01D90FC-6A0B-4B81-B37F-8CA9F7C3EDC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A01D90FC-6A0B-4B81-B37F-8CA9F7C3EDC4}.Release|Any CPU.Build.0 = Release|Any CPU + {F3366415-E934-4B9B-BAA5-9E6327DAD65D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3366415-E934-4B9B-BAA5-9E6327DAD65D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3366415-E934-4B9B-BAA5-9E6327DAD65D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3366415-E934-4B9B-BAA5-9E6327DAD65D}.Release|Any CPU.Build.0 = Release|Any CPU + {9D563A01-E981-47AB-9DC8-64E65BDE8949}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D563A01-E981-47AB-9DC8-64E65BDE8949}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D563A01-E981-47AB-9DC8-64E65BDE8949}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D563A01-E981-47AB-9DC8-64E65BDE8949}.Release|Any CPU.Build.0 = Release|Any CPU + {A97D552B-387E-4BB9-9D56-B6E1D950DE5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A97D552B-387E-4BB9-9D56-B6E1D950DE5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A97D552B-387E-4BB9-9D56-B6E1D950DE5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A97D552B-387E-4BB9-9D56-B6E1D950DE5B}.Release|Any CPU.Build.0 = Release|Any CPU + {1448F506-73E9-4664-A419-002B79A8C276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1448F506-73E9-4664-A419-002B79A8C276}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1448F506-73E9-4664-A419-002B79A8C276}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1448F506-73E9-4664-A419-002B79A8C276}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d8f3eff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3.8' +services: + pokedata_mssql: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: PokeData_mssql + ports: + - 27945:1433 + environment: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: m7tPnE6dB5TQxYCW + + pokedata_api: + build: + context: . + dockerfile: /src/PokeData/Dockerfile + image: pokedata_api + container_name: PokeData_api + depends_on: + - pokedata_mssql + restart: unless-stopped + environment: + ASPNETCORE_Environment: Development + SQLCONNSTR_Master: Server=pokedata_mssql,27945;Database=Pokemon;User Id=SA;Password=m7tPnE6dB5TQxYCW;Persist Security Info=False;Encrypt=False; + ports: + - 43551:8080 diff --git a/src/PokeData.Application/Caching/ICacheService.cs b/src/PokeData.Application/Caching/ICacheService.cs new file mode 100644 index 0000000..2d88626 --- /dev/null +++ b/src/PokeData.Application/Caching/ICacheService.cs @@ -0,0 +1,9 @@ +using PokeData.Domain.Resources; + +namespace PokeData.Application.Caching; + +public interface ICacheService +{ + Resource? GetResource(Uri source); + void SetResource(Resource resource); +} diff --git a/src/PokeData.Application/DependencyInjectionExtensions.cs b/src/PokeData.Application/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..c602ecc --- /dev/null +++ b/src/PokeData.Application/DependencyInjectionExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace PokeData.Application; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddPokeDataApplication(this IServiceCollection services) + { + return services.AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + } +} diff --git a/src/PokeData.Application/IResourceService.cs b/src/PokeData.Application/IResourceService.cs new file mode 100644 index 0000000..b6e24ea --- /dev/null +++ b/src/PokeData.Application/IResourceService.cs @@ -0,0 +1,8 @@ +using PokeData.Domain.Species; + +namespace PokeData.Application; + +public interface IResourceService +{ + Task GetSpeciesAsync(string id, CancellationToken cancellationToken = default); +} diff --git a/src/PokeData.Application/PokeData.Application.csproj b/src/PokeData.Application/PokeData.Application.csproj new file mode 100644 index 0000000..65f1d70 --- /dev/null +++ b/src/PokeData.Application/PokeData.Application.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + Nullable + + + + + + + + + + + + + + + + diff --git a/src/PokeData.Application/Species/Commands/ImportSpeciesCommand.cs b/src/PokeData.Application/Species/Commands/ImportSpeciesCommand.cs new file mode 100644 index 0000000..ef95d26 --- /dev/null +++ b/src/PokeData.Application/Species/Commands/ImportSpeciesCommand.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace PokeData.Application.Species.Commands; + +public record ImportSpeciesCommand(IEnumerable Ids) : INotification; diff --git a/src/PokeData.Domain/Generations/Generation.cs b/src/PokeData.Domain/Generations/Generation.cs new file mode 100644 index 0000000..22dad97 --- /dev/null +++ b/src/PokeData.Domain/Generations/Generation.cs @@ -0,0 +1,13 @@ +using PokeData.Domain.Regions; + +namespace PokeData.Domain.Generations; + +public class Generation +{ + public byte Number { get; set; } + + public string UniqueName { get; set; } = string.Empty; + public string? DisplayName { get; set; } + + public Region? MainRegion { get; set; } +} diff --git a/src/PokeData.Domain/PokeData.Domain.csproj b/src/PokeData.Domain/PokeData.Domain.csproj new file mode 100644 index 0000000..206e6dc --- /dev/null +++ b/src/PokeData.Domain/PokeData.Domain.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + Nullable + + + + + + + diff --git a/src/PokeData.Domain/PokemonType.cs b/src/PokeData.Domain/PokemonType.cs new file mode 100644 index 0000000..69c653d --- /dev/null +++ b/src/PokeData.Domain/PokemonType.cs @@ -0,0 +1,9 @@ +namespace PokeData.Domain; + +public class PokemonType +{ + public byte Number { get; set; } + + public string UniqueName { get; set; } = string.Empty; + public string? DisplayName { get; set; } +} diff --git a/src/PokeData.Domain/Regions/Region.cs b/src/PokeData.Domain/Regions/Region.cs new file mode 100644 index 0000000..fefd05c --- /dev/null +++ b/src/PokeData.Domain/Regions/Region.cs @@ -0,0 +1,13 @@ +using PokeData.Domain.Generations; + +namespace PokeData.Domain.Regions; + +public class Region +{ + public byte Number { get; set; } + + public string UniqueName { get; set; } = string.Empty; + public string? DisplayName { get; set; } + + public Generation? MainGeneration { get; set; } +} diff --git a/src/PokeData.Domain/Resources/Content.cs b/src/PokeData.Domain/Resources/Content.cs new file mode 100644 index 0000000..333c7aa --- /dev/null +++ b/src/PokeData.Domain/Resources/Content.cs @@ -0,0 +1,3 @@ +namespace PokeData.Domain.Resources; + +public record Content(string Type, string Text); diff --git a/src/PokeData.Domain/Resources/IResourceRepository.cs b/src/PokeData.Domain/Resources/IResourceRepository.cs new file mode 100644 index 0000000..ca295b0 --- /dev/null +++ b/src/PokeData.Domain/Resources/IResourceRepository.cs @@ -0,0 +1,8 @@ +namespace PokeData.Domain.Resources; + +public interface IResourceRepository +{ + Task> GetAsync(CancellationToken cancellationToken = default); + Task GetAsync(Uri source, CancellationToken cancellationToken = default); + Task SaveAsync(Resource resource, CancellationToken cancellationToken = default); +} diff --git a/src/PokeData.Domain/Resources/Resource.cs b/src/PokeData.Domain/Resources/Resource.cs new file mode 100644 index 0000000..89a750d --- /dev/null +++ b/src/PokeData.Domain/Resources/Resource.cs @@ -0,0 +1,3 @@ +namespace PokeData.Domain.Resources; + +public record Resource(Uri Source, Content Content); diff --git a/src/PokeData.Domain/Species/PokemonSpecies.cs b/src/PokeData.Domain/Species/PokemonSpecies.cs new file mode 100644 index 0000000..6312fdb --- /dev/null +++ b/src/PokeData.Domain/Species/PokemonSpecies.cs @@ -0,0 +1,29 @@ +using PokeData.Domain.Generations; + +namespace PokeData.Domain.Species; + +public class PokemonSpecies +{ + public ushort Number { get; set; } + public ushort Order { get; set; } + + public bool IsBaby { get; set; } + public bool IsLegendary { get; set; } + public bool IsMythical { get; set; } + public bool HasGenderDifferences { get; set; } + public bool CanSwitchForm { get; set; } + + public string UniqueName { get; set; } = string.Empty; + public string? DisplayName { get; set; } + public string? Category { get; set; } + + public double? GenderRatio { get; set; } + public byte CatchRate { get; set; } + public byte HatchTime { get; set; } + + public byte BaseFriendship { get; set; } + + public Generation? Generation { get; set; } + + public List Varieties { get; set; } = []; +} diff --git a/src/PokeData.Domain/Species/PokemonVariety.cs b/src/PokeData.Domain/Species/PokemonVariety.cs new file mode 100644 index 0000000..f926112 --- /dev/null +++ b/src/PokeData.Domain/Species/PokemonVariety.cs @@ -0,0 +1,20 @@ +namespace PokeData.Domain.Species; + +public class PokemonVariety +{ + public ushort Number { get; set; } + public ushort Order { get; set; } + + public string UniqueName { get; set; } = string.Empty; + + public PokemonType? PrimaryType { get; set; } + public PokemonType? SecondaryType { get; set; } + + public double Height { get; set; } + public double Weight { get; set; } + + public ushort BaseExperienceYield { get; set; } + + public PokemonSpecies? Species { get; set; } + public bool IsDefault { get; set; } +} diff --git a/src/PokeData.ETL/Entities/Generation.cs b/src/PokeData.ETL/Entities/Generation.cs deleted file mode 100644 index 86d29f2..0000000 --- a/src/PokeData.ETL/Entities/Generation.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text.Json.Serialization; - -namespace PokeData.ETL.Entities; - -internal class Generation -{ - public int Number { get; set; } - - public string UniqueName { get; set; } = string.Empty; - public string? DisplayName { get; set; } - - public Region? Region { get; set; } - - [JsonIgnore] - public List Species { get; set; } = []; - - public static Generation FromModel(Models.Generation model, string languageName) => new() - { - Number = model.Id, - UniqueName = model.Name, - DisplayName = model.Names.SingleOrDefault(name => name.Language?.Name == languageName)?.Value - }; -} diff --git a/src/PokeData.ETL/Entities/PokemonSpecies.cs b/src/PokeData.ETL/Entities/PokemonSpecies.cs deleted file mode 100644 index 35621f0..0000000 --- a/src/PokeData.ETL/Entities/PokemonSpecies.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace PokeData.ETL.Entities; - -internal class PokemonSpecies -{ - public ushort Number { get; set; } - public int Order { get; set; } - - public string UniqueName { get; set; } = string.Empty; - public string? DisplayName { get; set; } - public string? Category { get; set; } - - public double? GenderRatio { get; set; } - public byte CatchRate { get; set; } - - public byte HatchTime { get; set; } - - public byte BaseFriendship { get; set; } - - public bool IsBaby { get; set; } - public bool IsLegendary { get; set; } - public bool IsMythical { get; set; } - public bool HasGenderDifferences { get; set; } - public bool CanSwitchForm { get; set; } - - public Generation? Generation { get; set; } - public List Varieties { get; set; } = []; - - public static PokemonSpecies FromModel(Models.PokemonSpecies model, string languageName) => new() - { - Number = (ushort)model.Id, - Order = model.Order, - UniqueName = model.Name, - DisplayName = model.Names.SingleOrDefault(name => name.Language?.Name == languageName)?.Value, - Category = model.Genera.SingleOrDefault(genus => genus.Language?.Name == languageName)?.Value, - GenderRatio = model.GenderRate == -1 ? null : (1 - model.GenderRate / 8.0), - CatchRate = (byte)model.CaptureRate, - HatchTime = (byte?)model.HatchCounter ?? 0, - BaseFriendship = (byte?)model.BaseHappiness ?? 0, - IsBaby = model.IsBaby, - IsLegendary = model.IsLegendary, - IsMythical = model.IsMythical, - HasGenderDifferences = model.HasGenderDifferences, - CanSwitchForm = model.FormsSwitchable - }; -} diff --git a/src/PokeData.ETL/Entities/PokemonType.cs b/src/PokeData.ETL/Entities/PokemonType.cs deleted file mode 100644 index 7e9a7c1..0000000 --- a/src/PokeData.ETL/Entities/PokemonType.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace PokeData.ETL.Entities; - -internal class PokemonType -{ - public int Number { get; set; } - - public string UniqueName { get; set; } = string.Empty; - public string? DisplayName { get; set; } - - public static PokemonType FromModel(Models.Type model, string languageName) => new() - { - Number = model.Id, - UniqueName = model.Name, - DisplayName = model.Names.SingleOrDefault(name => name.Language?.Name == languageName)?.Value - }; -} diff --git a/src/PokeData.ETL/Entities/PokemonVariety.cs b/src/PokeData.ETL/Entities/PokemonVariety.cs deleted file mode 100644 index 049bb0d..0000000 --- a/src/PokeData.ETL/Entities/PokemonVariety.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json.Serialization; - -namespace PokeData.ETL.Entities; - -internal class PokemonVariety -{ - public int Number { get; set; } - public int Order { get; set; } - - public bool IsDefault { get; set; } - - public string UniqueName { get; set; } = string.Empty; - - public double Height { get; set; } - public double Weight { get; set; } - - public ushort BaseExperienceYield { get; set; } - - public List Types { get; set; } = []; - - [JsonIgnore] - public PokemonSpecies? Species { get; set; } - - public static PokemonVariety FromModel(Models.Pokemon model) => new() - { - Number = model.Id, - Order = model.Order, - IsDefault = model.IsDefault, - UniqueName = model.Name, - Height = model.Height / 10.0, - Weight = model.Weight / 10.0, - BaseExperienceYield = (ushort?)model.BaseExperience ?? 0 - }; -} diff --git a/src/PokeData.ETL/Entities/Region.cs b/src/PokeData.ETL/Entities/Region.cs deleted file mode 100644 index 4fdfc08..0000000 --- a/src/PokeData.ETL/Entities/Region.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace PokeData.ETL.Entities; - -internal class Region -{ - public int Number { get; set; } - - public string UniqueName { get; set; } = string.Empty; - public string? DisplayName { get; set; } - - [JsonIgnore] - public Generation? Generation { get; set; } - - public static Region FromModel(Models.Region model, string languageName) => new() - { - Number = model.Id, - UniqueName = model.Name, - DisplayName = model.Names.SingleOrDefault(name => name.Language?.Name == languageName)?.Value - }; -} diff --git a/src/PokeData.ETL/PokeData.ETL.csproj b/src/PokeData.ETL/PokeData.ETL.csproj index 4a6982c..47ecbea 100644 --- a/src/PokeData.ETL/PokeData.ETL.csproj +++ b/src/PokeData.ETL/PokeData.ETL.csproj @@ -11,9 +11,12 @@ - - + + + + + diff --git a/src/PokeData.ETL/Program.cs b/src/PokeData.ETL/Program.cs index 795d766..a8001ae 100644 --- a/src/PokeData.ETL/Program.cs +++ b/src/PokeData.ETL/Program.cs @@ -1,4 +1,7 @@ -namespace PokeData.ETL; +using PokeData.EntityFrameworkCore.SqlServer; +using PokeData.Infrastructure.PokeApiClient; + +namespace PokeData.ETL; public class Program { @@ -6,7 +9,9 @@ public static void Main(string[] args) { HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Services.AddHostedService(); - builder.Services.AddHttpClient(); + builder.Services.AddMemoryCache(); + builder.Services.AddPokeDataWithEntityFrameworkCoreSqlServer(builder.Configuration); + builder.Services.AddPokeDataWithPokeApiClient(); IHost host = builder.Build(); host.Run(); diff --git a/src/PokeData.ETL/SpeciesLine.cs b/src/PokeData.ETL/SpeciesLine.cs deleted file mode 100644 index aa06827..0000000 --- a/src/PokeData.ETL/SpeciesLine.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CsvHelper.Configuration.Attributes; - -namespace PokeData.ETL; - -internal record SpeciesLine -{ - [Index(0)] - public string? Region { get; set; } - - [Index(1)] - public string? Number { get; set; } - - [Index(2)] - public string? Name { get; set; } - - [Index(3)] - public string? PrimaryType { get; set; } - - [Index(4)] - public string? SecondaryType { get; set; } -} diff --git a/src/PokeData.ETL/Worker.cs b/src/PokeData.ETL/Worker.cs index 20cdaab..9a2d0bc 100644 --- a/src/PokeData.ETL/Worker.cs +++ b/src/PokeData.ETL/Worker.cs @@ -1,195 +1,42 @@ -using CsvHelper; -using PokeData.ETL.Entities; +using MediatR; +using PokeData.Application.Species.Commands; +using PokeData.Infrastructure.Commands; using System.Diagnostics; -using System.Globalization; -using System.Text; -using System.Text.Json; namespace PokeData.ETL; public class Worker : BackgroundService { - private const string ResourcesJsonPath = "resources.json"; - private const string SpeciesCsvPath = "species.csv"; - private const string SpeciesJsonPath = "species.json"; - - private static readonly CultureInfo Culture = CultureInfo.GetCultureInfo("en"); - private static readonly Encoding Encoding = Encoding.UTF8; - - private readonly HttpClient _client; private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; - private readonly Random _random = new(); - private Dictionary _resources = []; - private Dictionary _species = []; - - public Worker(HttpClient client, ILogger logger) + public Worker(ILogger logger, IServiceProvider serviceProvider) { - _client = client; _logger = logger; + _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { Stopwatch chrono = Stopwatch.StartNew(); - await LoadAsync(cancellationToken); - try { - for (int id = 1; id <= 1025; id++) - { - string speciesUrl = $"https://pokeapi.co/api/v2/pokemon-species/{id}/"; - Models.PokemonSpecies? speciesModel = await GetResourceAsync(speciesUrl, cancellationToken); - if (speciesModel == null) - { - _logger.LogWarning("No species found at '{url}'.", speciesUrl); - } - else - { - PokemonSpecies species = PokemonSpecies.FromModel(speciesModel, Culture.Name); - string key = $"urn:pokemon:species:id:{species.Number}"; - _species[key] = species; - - if (speciesModel.Generation != null) - { - Models.Generation? generationModel = await GetResourceAsync(speciesModel.Generation, cancellationToken); - if (generationModel != null) - { - Generation generation = Generation.FromModel(generationModel, Culture.Name); - species.Generation = generation; - if (generationModel.MainRegion != null) - { - Models.Region? regionModel = await GetResourceAsync(generationModel.MainRegion, cancellationToken); - if (regionModel != null) - { - generation.Region = Region.FromModel(regionModel, Culture.Name); - } - } - } - } + using IServiceScope scope = _serviceProvider.CreateScope(); - foreach (Models.PokemonSpeciesVariety speciesVariety in speciesModel.Varieties) - { - if (speciesVariety.Pokemon != null) - { - Models.Pokemon? varietyModel = await GetResourceAsync(speciesVariety.Pokemon, cancellationToken); - if (varietyModel != null) - { - PokemonVariety variety = PokemonVariety.FromModel(varietyModel); - species.Varieties.Add(variety); + IPublisher publisher = scope.ServiceProvider.GetRequiredService(); + await publisher.Publish(new InitializeDatabaseCommand(), cancellationToken); + await publisher.Publish(new InitializeCachingCommand(), cancellationToken); - IOrderedEnumerable pokemonTypes = varietyModel.Types.OrderBy(type => type.Slot); - foreach (Models.PokemonType pokemonType in pokemonTypes) - { - if (pokemonType.Type != null) - { - Models.Type? typeModel = await GetResourceAsync(pokemonType.Type, cancellationToken); - if (typeModel != null) - { - PokemonType type = PokemonType.FromModel(typeModel, Culture.Name); - variety.Types.Add(type); - } - } - } - } - } - } - } - } + IEnumerable ids = Enumerable.Range(1, 1025).Select(id => id.ToString()); + await publisher.Publish(new ImportSpeciesCommand(ids), cancellationToken); } catch (Exception exception) { _logger.LogError(exception, "An unhandled exception has occurred."); } - await SaveAsync(cancellationToken); - chrono.Stop(); _logger.LogInformation("Operation completed in {elapsed}ms.", chrono.ElapsedMilliseconds); } - - private async Task GetResourceAsync(Models.APIResource resource, CancellationToken cancellationToken) - => await GetResourceAsync(resource.Url, cancellationToken); - private async Task GetResourceAsync(string url, CancellationToken cancellationToken) - { - if (!_resources.TryGetValue(url, out string? json)) - { - int millisecondsDelay = _random.Next(1, 10 + 1) * 100; - await Task.Delay(millisecondsDelay, cancellationToken); - - Uri requestUri = new(url); - using HttpRequestMessage request = new(HttpMethod.Get, requestUri); - using HttpResponseMessage response = await _client.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - - json = await response.Content.ReadAsStringAsync(cancellationToken); - _resources[url] = json; - } - - return JsonSerializer.Deserialize(json); - } - - private async Task LoadAsync(CancellationToken cancellationToken) - { - try - { - string resources = await File.ReadAllTextAsync(ResourcesJsonPath, Encoding, cancellationToken); - _resources = JsonSerializer.Deserialize>(resources) ?? []; - } - catch (Exception) - { - } - - try - { - string species = await File.ReadAllTextAsync(SpeciesJsonPath, Encoding, cancellationToken); - _species = JsonSerializer.Deserialize>(species) ?? []; - } - catch (Exception) - { - } - } - private async Task SaveAsync(CancellationToken cancellationToken) - { - string resources = JsonSerializer.Serialize(_resources); - await File.WriteAllTextAsync(ResourcesJsonPath, resources, Encoding, cancellationToken); - - string species = JsonSerializer.Serialize(_species); - await File.WriteAllTextAsync(SpeciesJsonPath, species, Encoding, cancellationToken); - - IEnumerable records = _species.Values.Select(species => - { - Region? region = species.Generation?.Region; - PokemonVariety? variety = species.Varieties.SingleOrDefault(variety => variety.IsDefault); - - SpeciesLine line = new() - { - Number = species.Number.ToString("0000"), - Name = species.DisplayName ?? species.UniqueName - }; - - if (region != null) - { - line.Region = region.DisplayName ?? region.UniqueName; - } - - if (variety != null) - { - PokemonType primaryType = variety.Types.First(); - line.PrimaryType = primaryType.DisplayName ?? primaryType.UniqueName; - - if (variety.Types.Count > 1) - { - PokemonType secondaryType = variety.Types.Skip(1).Single(); - line.SecondaryType = secondaryType.DisplayName ?? secondaryType.UniqueName; - } - } - - return line; - }); - using StreamWriter writer = new(SpeciesCsvPath); - using CsvWriter csv = new(writer, Culture); - csv.WriteRecords(records); - } } diff --git a/src/PokeData.ETL/appsettings.Development.json b/src/PokeData.ETL/appsettings.Development.json index b2dcdb6..0324a72 100644 --- a/src/PokeData.ETL/appsettings.Development.json +++ b/src/PokeData.ETL/appsettings.Development.json @@ -1,8 +1,13 @@ { + "Caching": { + "LoadResourcesOnStartup": true + }, + "EnableMigrations": true, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.Hosting.Lifetime": "Information" } - } + }, + "SQLCONNSTR_Pokemon": "Server=host.docker.internal,27945;Database=Pokemon;User Id=SA;Password=m7tPnE6dB5TQxYCW;Persist Security Info=False;Encrypt=False;" } diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/AggregateConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/AggregateConfiguration.cs new file mode 100644 index 0000000..338974e --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/AggregateConfiguration.cs @@ -0,0 +1,22 @@ +using Logitar.EventSourcing; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal abstract class AggregateConfiguration where T : AggregateEntity +{ + public virtual void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(x => x.AggregateId).IsUnique(); + builder.HasIndex(x => x.Version); + builder.HasIndex(x => x.CreatedBy); + builder.HasIndex(x => x.CreatedOn); + builder.HasIndex(x => x.UpdatedBy); + builder.HasIndex(x => x.UpdatedOn); + + builder.Property(x => x.AggregateId).HasMaxLength(AggregateId.MaximumLength); + builder.Property(x => x.CreatedBy).HasMaxLength(ActorId.MaximumLength); + builder.Property(x => x.UpdatedBy).HasMaxLength(ActorId.MaximumLength); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/GenerationConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/GenerationConfiguration.cs new file mode 100644 index 0000000..c7665a4 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/GenerationConfiguration.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class GenerationConfiguration : AggregateConfiguration, IEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.ToTable(nameof(PokemonContext.Generations)); + builder.HasKey(x => x.GenerationId); + + builder.HasIndex(x => x.UniqueName); + builder.HasIndex(x => x.UniqueNameNormalized).IsUnique(); + builder.HasIndex(x => x.DisplayName); + + builder.Property(x => x.GenerationId).ValueGeneratedNever(); + builder.Property(x => x.UniqueName).HasMaxLength(128); + builder.Property(x => x.UniqueNameNormalized).HasMaxLength(128); + builder.Property(x => x.DisplayName).HasMaxLength(128); + + builder.HasOne(x => x.MainRegion).WithOne(x => x.MainGeneration) + .HasPrincipalKey(x => x.RegionId).HasForeignKey(x => x.GenerationId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonSpeciesConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonSpeciesConfiguration.cs new file mode 100644 index 0000000..e22721f --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonSpeciesConfiguration.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class PokemonSpeciesConfiguration : AggregateConfiguration, IEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.ToTable(nameof(PokemonContext.PokemonSpecies)); + builder.HasKey(x => x.PokemonSpeciesId); + + builder.HasIndex(x => x.Order); + builder.HasIndex(x => x.IsBaby); + builder.HasIndex(x => x.IsLegendary); + builder.HasIndex(x => x.IsMythical); + builder.HasIndex(x => x.HasGenderDifferences); + builder.HasIndex(x => x.CanSwitchForm); + builder.HasIndex(x => x.UniqueName); + builder.HasIndex(x => x.UniqueNameNormalized).IsUnique(); + builder.HasIndex(x => x.DisplayName); + builder.HasIndex(x => x.Category); + + builder.Property(x => x.PokemonSpeciesId).ValueGeneratedNever(); + builder.Property(x => x.UniqueName).HasMaxLength(128); + builder.Property(x => x.UniqueNameNormalized).HasMaxLength(128); + builder.Property(x => x.DisplayName).HasMaxLength(128); + builder.Property(x => x.Category).HasMaxLength(128); + + builder.HasOne(x => x.Generation).WithMany(x => x.Species) + .HasPrincipalKey(x => x.GenerationId).HasForeignKey(x => x.GenerationId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonTypeConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonTypeConfiguration.cs new file mode 100644 index 0000000..dbb8fb1 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonTypeConfiguration.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class PokemonTypeConfiguration : AggregateConfiguration, IEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.ToTable(nameof(PokemonContext.PokemonTypes)); + builder.HasKey(x => x.PokemonTypeId); + + builder.HasIndex(x => x.UniqueName); + builder.HasIndex(x => x.UniqueNameNormalized).IsUnique(); + builder.HasIndex(x => x.DisplayName); + + builder.Property(x => x.PokemonTypeId).ValueGeneratedNever(); + builder.Property(x => x.UniqueName).HasMaxLength(128); + builder.Property(x => x.UniqueNameNormalized).HasMaxLength(128); + builder.Property(x => x.DisplayName).HasMaxLength(128); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonVarietyConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonVarietyConfiguration.cs new file mode 100644 index 0000000..e2106ec --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/PokemonVarietyConfiguration.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class PokemonVarietyConfiguration : AggregateConfiguration, IEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.ToTable(nameof(PokemonContext.PokemonVarieties)); + builder.HasKey(x => x.PokemonVarietyId); + + builder.HasIndex(x => x.Order); + builder.HasIndex(x => x.UniqueName); + builder.HasIndex(x => x.UniqueNameNormalized).IsUnique(); + builder.HasIndex(x => x.IsDefault); + + builder.Property(x => x.PokemonVarietyId).ValueGeneratedNever(); + builder.Property(x => x.UniqueName).HasMaxLength(128); + builder.Property(x => x.UniqueNameNormalized).HasMaxLength(128); + + builder.HasOne(x => x.Species).WithMany(x => x.Varieties) + .HasPrincipalKey(x => x.PokemonSpeciesId).HasForeignKey(x => x.PokemonSpeciesId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/RegionConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/RegionConfiguration.cs new file mode 100644 index 0000000..e4a1d40 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/RegionConfiguration.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class RegionConfiguration : AggregateConfiguration, IEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.ToTable(nameof(PokemonContext.Regions)); + builder.HasKey(x => x.RegionId); + + builder.HasIndex(x => x.UniqueName); + builder.HasIndex(x => x.UniqueNameNormalized).IsUnique(); + builder.HasIndex(x => x.DisplayName); + + builder.Property(x => x.RegionId).ValueGeneratedNever(); + builder.Property(x => x.UniqueName).HasMaxLength(128); + builder.Property(x => x.UniqueNameNormalized).HasMaxLength(128); + builder.Property(x => x.DisplayName).HasMaxLength(128); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/ResourceConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/ResourceConfiguration.cs new file mode 100644 index 0000000..dd07f80 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/ResourceConfiguration.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class ResourceConfiguration : AggregateConfiguration, IEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.ToTable(nameof(PokemonContext.Resources)); + builder.HasKey(x => x.ResourceId); + + builder.HasIndex(x => x.Source); + builder.HasIndex(x => x.SourceNormalized).IsUnique(); + builder.HasIndex(x => x.ContentType); + + builder.Property(x => x.Source).HasMaxLength(2048); + builder.Property(x => x.SourceNormalized).HasMaxLength(2048); + builder.Property(x => x.ContentType).HasMaxLength(byte.MaxValue); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/DependencyInjectionExtensions.cs b/src/PokeData.EntityFrameworkCore.Relational/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..7eaaad6 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/DependencyInjectionExtensions.cs @@ -0,0 +1,24 @@ +using Logitar.EventSourcing.EntityFrameworkCore.Relational; +using Microsoft.Extensions.DependencyInjection; +using PokeData.Domain.Resources; +using PokeData.EntityFrameworkCore.Relational.Repositories; +using PokeData.Infrastructure; + +namespace PokeData.EntityFrameworkCore.Relational; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddPokeDataWithEntityFrameworkCoreRelational(this IServiceCollection services) + { + return services + .AddLogitarEventSourcingWithEntityFrameworkCoreRelational() + .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) + .AddPokeDataInfrastructure() + .AddRepositories(); + } + + private static IServiceCollection AddRepositories(this IServiceCollection services) + { + return services.AddTransient(); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/AggregateEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/AggregateEntity.cs new file mode 100644 index 0000000..d528c70 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/AggregateEntity.cs @@ -0,0 +1,37 @@ +using Logitar.EventSourcing; + +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal abstract class AggregateEntity +{ + public string AggregateId { get; protected set; } = string.Empty; + public long Version { get; protected set; } + + public string CreatedBy { get; protected set; } = string.Empty; + public DateTime CreatedOn { get; protected set; } + + public string UpdatedBy { get; protected set; } = string.Empty; + public DateTime UpdatedOn { get; protected set; } + + protected AggregateEntity() + { + } + + protected AggregateEntity(DomainEvent @event) + { + AggregateId = @event.AggregateId.Value; + + CreatedBy = @event.ActorId.Value; + CreatedOn = @event.OccurredOn.ToUniversalTime(); + + Update(@event); + } + + protected void Update(DomainEvent @event) + { + Version = @event.Version; + + UpdatedBy = @event.ActorId.Value; + UpdatedOn = @event.OccurredOn.ToUniversalTime(); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/GenerationEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/GenerationEntity.cs new file mode 100644 index 0000000..b8c1532 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/GenerationEntity.cs @@ -0,0 +1,56 @@ +using Logitar.EventSourcing; +using PokeData.Domain.Generations; + +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class GenerationEntity : AggregateEntity +{ + public int GenerationId { get; private set; } + + public string UniqueName { get; private set; } = string.Empty; + public string UniqueNameNormalized + { + get => UniqueName.ToUpper(); + private set { } + } + public string? DisplayName { get; private set; } + + public RegionEntity? MainRegion { get; private set; } + public int MainRegionId { get; private set; } + + public List Species { get; private set; } = []; + + public GenerationEntity(Generation generation, RegionEntity mainRegion) + { + AggregateId = Logitar.EventSourcing.AggregateId.NewId().ToString(); + Version = 1; + CreatedBy = UpdatedBy = ActorId.DefaultValue; + CreatedOn = UpdatedOn = DateTime.UtcNow; + + GenerationId = generation.Number; + + UniqueName = generation.UniqueName; + DisplayName = generation.DisplayName; + + MainRegion = mainRegion; + MainRegionId = mainRegion.RegionId; + + } + + private GenerationEntity() + { + } + + public void Update(Generation generation) + { + if (generation.UniqueName != UniqueName || generation.DisplayName != DisplayName) + { + Version++; + UpdatedBy = ActorId.DefaultValue; + UpdatedOn = DateTime.UtcNow; + + UniqueName = generation.UniqueName; + DisplayName = generation.DisplayName; + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonSpeciesEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonSpeciesEntity.cs new file mode 100644 index 0000000..2f0e3c9 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonSpeciesEntity.cs @@ -0,0 +1,108 @@ +using Logitar.EventSourcing; +using PokeData.Domain.Species; + +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class PokemonSpeciesEntity : AggregateEntity +{ + public int PokemonSpeciesId { get; private set; } + + public GenerationEntity? Generation { get; private set; } + public int GenerationId { get; private set; } + + public int Order { get; private set; } + + public bool IsBaby { get; private set; } + public bool IsLegendary { get; private set; } + public bool IsMythical { get; private set; } + public bool HasGenderDifferences { get; private set; } + public bool CanSwitchForm { get; private set; } + + public string UniqueName { get; private set; } = string.Empty; + public string UniqueNameNormalized + { + get => UniqueName.ToUpper(); + private set { } + } + public string? DisplayName { get; private set; } + public string? Category { get; private set; } + + public double? GenderRatio { get; private set; } + public byte CatchRate { get; private set; } + public byte HatchTime { get; private set; } + + public byte BaseFriendship { get; private set; } + + public List Varieties { get; private set; } = []; + + public PokemonSpeciesEntity(PokemonSpecies species, GenerationEntity generation) + { + AggregateId = Logitar.EventSourcing.AggregateId.NewId().ToString(); + Version = 1; + CreatedBy = UpdatedBy = ActorId.DefaultValue; + CreatedOn = UpdatedOn = DateTime.UtcNow; + + PokemonSpeciesId = species.Number; + + Generation = generation; + GenerationId = generation.GenerationId; + + Order = species.Order; + + IsBaby = species.IsBaby; + IsLegendary = species.IsLegendary; + IsMythical = species.IsMythical; + HasGenderDifferences = species.HasGenderDifferences; + CanSwitchForm = species.CanSwitchForm; + + UniqueName = species.UniqueName; + DisplayName = species.DisplayName; + Category = species.Category; + + GenderRatio = species.GenderRatio; + CatchRate = species.CatchRate; + HatchTime = species.HatchTime; + + BaseFriendship = species.BaseFriendship; + } + + private PokemonSpeciesEntity() + { + } + + public void Update(PokemonSpecies species, GenerationEntity generation) + { + if (generation.GenerationId != GenerationId || species.Order != Order + || species.IsBaby != IsBaby || species.IsLegendary != IsLegendary || species.IsMythical != IsMythical + || species.HasGenderDifferences != HasGenderDifferences || species.CanSwitchForm != CanSwitchForm + || species.UniqueName != UniqueName || species.DisplayName != DisplayName || species.Category != Category + || species.GenderRatio != GenderRatio || species.CatchRate != CatchRate || species.HatchTime != HatchTime + || species.BaseFriendship != BaseFriendship) + { + Version++; + UpdatedBy = ActorId.DefaultValue; + UpdatedOn = DateTime.UtcNow; + + Generation = generation; + GenerationId = generation.GenerationId; + + Order = species.Order; + + IsBaby = species.IsBaby; + IsLegendary = species.IsLegendary; + IsMythical = species.IsMythical; + HasGenderDifferences = species.HasGenderDifferences; + CanSwitchForm = species.CanSwitchForm; + + UniqueName = species.UniqueName; + DisplayName = species.DisplayName; + Category = species.Category; + + GenderRatio = species.GenderRatio; + CatchRate = species.CatchRate; + HatchTime = species.HatchTime; + + BaseFriendship = species.BaseFriendship; + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonTypeEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonTypeEntity.cs new file mode 100644 index 0000000..3b32275 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonTypeEntity.cs @@ -0,0 +1,47 @@ +using Logitar.EventSourcing; +using PokeData.Domain; + +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class PokemonTypeEntity : AggregateEntity +{ + public int PokemonTypeId { get; private set; } + + public string UniqueName { get; private set; } = string.Empty; + public string UniqueNameNormalized + { + get => UniqueName.ToUpper(); + private set { } + } + public string? DisplayName { get; private set; } + + public PokemonTypeEntity(PokemonType pokemonType) + { + AggregateId = Logitar.EventSourcing.AggregateId.NewId().ToString(); + Version = 1; + CreatedBy = UpdatedBy = ActorId.DefaultValue; + CreatedOn = UpdatedOn = DateTime.UtcNow; + + PokemonTypeId = pokemonType.Number; + + UniqueName = pokemonType.UniqueName; + DisplayName = pokemonType.DisplayName; + } + + private PokemonTypeEntity() + { + } + + public void Update(PokemonType pokemonType) + { + if (pokemonType.UniqueName != UniqueName || pokemonType.DisplayName != DisplayName) + { + Version++; + UpdatedBy = ActorId.DefaultValue; + UpdatedOn = DateTime.UtcNow; + + UniqueName = pokemonType.UniqueName; + DisplayName = pokemonType.DisplayName; + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonVarietyEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonVarietyEntity.cs new file mode 100644 index 0000000..1a52a3d --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/PokemonVarietyEntity.cs @@ -0,0 +1,93 @@ +using Logitar.EventSourcing; +using PokeData.Domain.Species; + +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class PokemonVarietyEntity : AggregateEntity +{ + public int PokemonVarietyId { get; private set; } + + public int Order { get; private set; } + + public string UniqueName { get; private set; } = string.Empty; + public string UniqueNameNormalized + { + get => UniqueName.ToUpper(); + private set { } + } + + public PokemonTypeEntity? PrimaryType { get; private set; } + public int PrimaryTypeId { get; private set; } + public PokemonTypeEntity? SecondaryType { get; private set; } + public int? SecondaryTypeId { get; private set; } + + public double Height { get; private set; } + public double Weight { get; private set; } + + public ushort BaseExperienceYield { get; private set; } + + public PokemonSpeciesEntity? Species { get; private set; } + public int PokemonSpeciesId { get; private set; } + public bool IsDefault { get; private set; } + + public PokemonVarietyEntity(PokemonVariety variety, PokemonSpeciesEntity species, PokemonTypeEntity primaryType, PokemonTypeEntity? secondaryType) + { + AggregateId = Logitar.EventSourcing.AggregateId.NewId().ToString(); + Version = 1; + CreatedBy = UpdatedBy = ActorId.DefaultValue; + CreatedOn = UpdatedOn = DateTime.UtcNow; + + PokemonVarietyId = variety.Number; + + Order = variety.Order; + + UniqueName = variety.UniqueName; + + PrimaryType = primaryType; + PrimaryTypeId = primaryType.PokemonTypeId; + SecondaryType = secondaryType; + SecondaryTypeId = secondaryType?.PokemonTypeId; + + Height = variety.Height; + Weight = variety.Weight; + + BaseExperienceYield = variety.BaseExperienceYield; + + Species = species; + PokemonSpeciesId = species.PokemonSpeciesId; + IsDefault = variety.IsDefault; + } + + private PokemonVarietyEntity() + { + } + + public void Update(PokemonVariety variety, PokemonTypeEntity primaryType, PokemonTypeEntity? secondaryType) + { + if (variety.Order != Order || variety.UniqueName != UniqueName + || primaryType.PokemonTypeId != PrimaryTypeId || secondaryType?.PokemonTypeId != SecondaryTypeId + || variety.Height != Height || variety.Weight != Weight + || variety.BaseExperienceYield != BaseExperienceYield || variety.IsDefault != IsDefault) + { + Version++; + UpdatedBy = ActorId.DefaultValue; + UpdatedOn = DateTime.UtcNow; + + Order = variety.Order; + + UniqueName = variety.UniqueName; + + PrimaryType = primaryType; + PrimaryTypeId = primaryType.PokemonTypeId; + SecondaryType = secondaryType; + SecondaryTypeId = secondaryType?.PokemonTypeId; + + Height = variety.Height; + Weight = variety.Weight; + + BaseExperienceYield = variety.BaseExperienceYield; + + IsDefault = variety.IsDefault; + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/RegionEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/RegionEntity.cs new file mode 100644 index 0000000..549bc11 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/RegionEntity.cs @@ -0,0 +1,50 @@ +using Logitar.EventSourcing; +using PokeData.Domain.Regions; + +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class RegionEntity : AggregateEntity +{ + public int RegionId { get; private set; } + + public string UniqueName { get; private set; } = string.Empty; + public string UniqueNameNormalized + { + get => UniqueName.ToUpper(); + private set { } + } + public string? DisplayName { get; private set; } + + public GenerationEntity? MainGeneration { get; private set; } + public int? MainGenerationId { get; private set; } + + public RegionEntity(Region generation) + { + AggregateId = Logitar.EventSourcing.AggregateId.NewId().ToString(); + Version = 1; + CreatedBy = UpdatedBy = ActorId.DefaultValue; + CreatedOn = UpdatedOn = DateTime.UtcNow; + + RegionId = generation.Number; + + UniqueName = generation.UniqueName; + DisplayName = generation.DisplayName; + } + + private RegionEntity() + { + } + + public void Update(Region generation) + { + if (generation.UniqueName != UniqueName || generation.DisplayName != DisplayName) + { + Version++; + UpdatedBy = ActorId.DefaultValue; + UpdatedOn = DateTime.UtcNow; + + UniqueName = generation.UniqueName; + DisplayName = generation.DisplayName; + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/ResourceEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/ResourceEntity.cs new file mode 100644 index 0000000..2c86b7d --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/ResourceEntity.cs @@ -0,0 +1,49 @@ +using Logitar.EventSourcing; +using PokeData.Domain.Resources; + +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class ResourceEntity : AggregateEntity +{ + public int ResourceId { get; private set; } + + public string Source { get; private set; } = string.Empty; + public string SourceNormalized + { + get => Source.ToUpper(); + private set { } + } + + public string ContentType { get; private set; } = string.Empty; + public string ContentText { get; private set; } = string.Empty; + + public ResourceEntity(Resource resource) : base() + { + AggregateId = Logitar.EventSourcing.AggregateId.NewId().ToString(); + Version = 1; + CreatedBy = UpdatedBy = ActorId.DefaultValue; + CreatedOn = UpdatedOn = DateTime.UtcNow; + + Source = resource.Source.ToString(); + + ContentType = resource.Content.Type; + ContentText = resource.Content.Text; + } + + private ResourceEntity() : base() + { + } + + public void Update(Resource resource) + { + if (resource.Content.Type != ContentType || resource.Content.Text != ContentText) + { + Version++; + UpdatedBy = ActorId.DefaultValue; + UpdatedOn = DateTime.UtcNow; + + ContentType = resource.Content.Type; + ContentText = resource.Content.Text; + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Handlers/InitializeDatabaseCommandHandler.cs b/src/PokeData.EntityFrameworkCore.Relational/Handlers/InitializeDatabaseCommandHandler.cs new file mode 100644 index 0000000..a9aa415 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Handlers/InitializeDatabaseCommandHandler.cs @@ -0,0 +1,30 @@ +using Logitar.EventSourcing.EntityFrameworkCore.Relational; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using PokeData.Infrastructure.Commands; + +namespace PokeData.EntityFrameworkCore.Relational.Handlers; + +internal class InitializeDatabaseCommandHandler : INotificationHandler +{ + private readonly IConfiguration _configuration; + private readonly EventContext _eventContext; + private readonly PokemonContext _pokemonContext; + + public InitializeDatabaseCommandHandler(IConfiguration configuration, EventContext eventContext, PokemonContext pokemonContext) + { + _configuration = configuration; + _eventContext = eventContext; + _pokemonContext = pokemonContext; + } + + public async Task Handle(InitializeDatabaseCommand _, CancellationToken cancellationToken) + { + if (_configuration.GetValue("EnableMigrations")) + { + await _eventContext.Database.MigrateAsync(cancellationToken); + await _pokemonContext.Database.MigrateAsync(cancellationToken); + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Handlers/Species/ImportSpeciesCommandHandler.cs b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Species/ImportSpeciesCommandHandler.cs new file mode 100644 index 0000000..51349c5 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Species/ImportSpeciesCommandHandler.cs @@ -0,0 +1,148 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using PokeData.Application; +using PokeData.Application.Species.Commands; +using PokeData.Domain.Species; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Handlers.Species; + +internal class ImportSpeciesCommandHandler : INotificationHandler +{ + private readonly PokemonContext _context; + private readonly IResourceService _resourceService; + + public ImportSpeciesCommandHandler(PokemonContext context, IResourceService resourceService) + { + _context = context; + _resourceService = resourceService; + } + + public async Task Handle(ImportSpeciesCommand command, CancellationToken cancellationToken) + { + IEnumerable species = await ExtractAsync(command.Ids, cancellationToken); + await LoadAsync(species, cancellationToken); + } + + private async Task> ExtractAsync(IEnumerable ids, CancellationToken cancellationToken) + { + List speciesList = new(capacity: ids.Count()); + + foreach (string id in ids) + { + PokemonSpecies? species = await _resourceService.GetSpeciesAsync(id, cancellationToken) + ?? throw new InvalidOperationException($"The Pokémon species 'Id={id}' could not be found."); + speciesList.Add(species); + } + + return speciesList.AsReadOnly(); + } + + private async Task LoadAsync(IEnumerable speciesList, CancellationToken cancellationToken) + { + Dictionary generations = await _context.Generations + .ToDictionaryAsync(x => x.GenerationId, x => x, cancellationToken); + Dictionary regions = await _context.Regions + .ToDictionaryAsync(x => x.RegionId, x => x, cancellationToken); + Dictionary speciesEntities = await _context.PokemonSpecies + .ToDictionaryAsync(x => x.PokemonSpeciesId, x => x, cancellationToken); + Dictionary types = await _context.PokemonTypes + .ToDictionaryAsync(x => x.PokemonTypeId, x => x, cancellationToken); + Dictionary varieties = await _context.PokemonVarieties + .ToDictionaryAsync(x => x.PokemonVarietyId, x => x, cancellationToken); + + foreach (PokemonSpecies species in speciesList) + { + if (species.Generation != null) + { + if (generations.TryGetValue(species.Generation.Number, out GenerationEntity? generation)) + { + generation.Update(species.Generation); + } + else if (species.Generation.MainRegion != null) + { + if (regions.TryGetValue(species.Generation.MainRegion.Number, out RegionEntity? region)) + { + region.Update(species.Generation.MainRegion); + } + else + { + region = new(species.Generation.MainRegion); + regions[region.RegionId] = region; + + _context.Regions.Add(region); + } + + generation = new(species.Generation, region); + generations[generation.GenerationId] = generation; + + _context.Generations.Add(generation); + } + else + { + continue; + } + + if (speciesEntities.TryGetValue(species.Number, out PokemonSpeciesEntity? speciesEntity)) + { + speciesEntity.Update(species, generation); + } + else + { + speciesEntity = new(species, generation); + speciesEntities[speciesEntity.PokemonSpeciesId] = speciesEntity; + + _context.PokemonSpecies.Add(speciesEntity); + } + + foreach (PokemonVariety variety in species.Varieties) + { + if (variety.PrimaryType != null) + { + if (types.TryGetValue(variety.PrimaryType.Number, out PokemonTypeEntity? primaryType)) + { + primaryType.Update(variety.PrimaryType); + } + else + { + primaryType = new(variety.PrimaryType); + types[primaryType.PokemonTypeId] = primaryType; + + _context.PokemonTypes.Add(primaryType); + } + + PokemonTypeEntity? secondaryType = null; + if (variety.SecondaryType != null) + { + if (types.TryGetValue(variety.SecondaryType.Number, out secondaryType)) + { + secondaryType.Update(variety.SecondaryType); + } + else + { + secondaryType = new(variety.SecondaryType); + types[secondaryType.PokemonTypeId] = secondaryType; + + _context.PokemonTypes.Add(secondaryType); + } + } + + if (varieties.TryGetValue(variety.Number, out PokemonVarietyEntity? varietyEntity)) + { + varietyEntity.Update(variety, primaryType, secondaryType); + } + else + { + varietyEntity = new(variety, speciesEntity, primaryType, secondaryType); + varieties[varietyEntity.PokemonVarietyId] = varietyEntity; + + speciesEntity.Varieties.Add(varietyEntity); + } + } + } + + await _context.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/PokeData.EntityFrameworkCore.Relational.csproj b/src/PokeData.EntityFrameworkCore.Relational/PokeData.EntityFrameworkCore.Relational.csproj new file mode 100644 index 0000000..a7ea086 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/PokeData.EntityFrameworkCore.Relational.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + Nullable + + + + + + + + + + + + + + + diff --git a/src/PokeData.EntityFrameworkCore.Relational/PokemonContext.cs b/src/PokeData.EntityFrameworkCore.Relational/PokemonContext.cs new file mode 100644 index 0000000..f867a36 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/PokemonContext.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational; + +public class PokemonContext : DbContext +{ + public PokemonContext(DbContextOptions options) : base(options) + { + } + + internal DbSet Generations { get; private set; } + internal DbSet PokemonSpecies { get; private set; } + internal DbSet PokemonTypes { get; private set; } + internal DbSet PokemonVarieties { get; private set; } + internal DbSet Regions { get; private set; } + internal DbSet Resources { get; private set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Repositories/ResourceRepository.cs b/src/PokeData.EntityFrameworkCore.Relational/Repositories/ResourceRepository.cs new file mode 100644 index 0000000..467bde8 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Repositories/ResourceRepository.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore; +using PokeData.Domain.Resources; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Repositories; + +internal class ResourceRepository : IResourceRepository +{ + private readonly PokemonContext _context; + + public ResourceRepository(PokemonContext context) + { + _context = context; + } + + public async Task> GetAsync(CancellationToken cancellationToken) + { + ResourceEntity[] entities = await _context.Resources.AsNoTracking() + .ToArrayAsync(cancellationToken); + + return entities.Select(Map); + } + + public async Task GetAsync(Uri source, CancellationToken cancellationToken) + { + string sourceNormalized = source.ToString().ToUpper(); + + ResourceEntity? entity = await _context.Resources.AsNoTracking() + .SingleOrDefaultAsync(x => x.SourceNormalized == sourceNormalized, cancellationToken); + + return entity == null ? null : Map(entity); + } + + public async Task SaveAsync(Resource resource, CancellationToken cancellationToken) + { + string sourceNormalized = resource.Source.ToString().ToUpper(); + ResourceEntity? entity = await _context.Resources + .SingleOrDefaultAsync(x => x.SourceNormalized == sourceNormalized, cancellationToken); + if (entity == null) + { + entity = new(resource); + _context.Resources.Add(entity); + } + else + { + entity.Update(resource); + } + + await _context.SaveChangesAsync(cancellationToken); + } + + private static Resource Map(ResourceEntity resource) => new(new Uri(resource.Source), new Content(resource.ContentType, resource.ContentText)); +} diff --git a/src/PokeData.EntityFrameworkCore.SqlServer/DependencyInjectionExtensions.cs b/src/PokeData.EntityFrameworkCore.SqlServer/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..da72095 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.SqlServer/DependencyInjectionExtensions.cs @@ -0,0 +1,24 @@ +using Logitar.EventSourcing.EntityFrameworkCore.SqlServer; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using PokeData.EntityFrameworkCore.Relational; + +namespace PokeData.EntityFrameworkCore.SqlServer; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddPokeDataWithEntityFrameworkCoreSqlServer(this IServiceCollection services, IConfiguration configuration) + { + string connectionString = configuration.GetValue("SQLCONNSTR_Pokemon") ?? string.Empty; + + return services.AddPokeDataWithEntityFrameworkCoreSqlServer(connectionString); + } + public static IServiceCollection AddPokeDataWithEntityFrameworkCoreSqlServer(this IServiceCollection services, string connectionString) + { + return services + .AddDbContext(options => options.UseSqlServer(connectionString, b => b.MigrationsAssembly("PokeData.EntityFrameworkCore.SqlServer"))) + .AddLogitarEventSourcingWithEntityFrameworkCoreSqlServer(connectionString) + .AddPokeDataWithEntityFrameworkCoreRelational(); + } +} diff --git a/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/20231231153029_InitialMigration.Designer.cs b/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/20231231153029_InitialMigration.Designer.cs new file mode 100644 index 0000000..3d4cdef --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/20231231153029_InitialMigration.Designer.cs @@ -0,0 +1,595 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PokeData.EntityFrameworkCore.Relational; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.SqlServer.Migrations +{ + [DbContext(typeof(PokemonContext))] + [Migration("20231231153029_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.Property("GenerationId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MainRegionId") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("GenerationId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Generations", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", b => + { + b.Property("PokemonSpeciesId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("BaseFriendship") + .HasColumnType("tinyint"); + + b.Property("CanSwitchForm") + .HasColumnType("bit"); + + b.Property("CatchRate") + .HasColumnType("tinyint"); + + b.Property("Category") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("GenderRatio") + .HasColumnType("float"); + + b.Property("GenerationId") + .HasColumnType("int"); + + b.Property("HasGenderDifferences") + .HasColumnType("bit"); + + b.Property("HatchTime") + .HasColumnType("tinyint"); + + b.Property("IsBaby") + .HasColumnType("bit"); + + b.Property("IsLegendary") + .HasColumnType("bit"); + + b.Property("IsMythical") + .HasColumnType("bit"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("PokemonSpeciesId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CanSwitchForm"); + + b.HasIndex("Category"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("GenerationId"); + + b.HasIndex("HasGenderDifferences"); + + b.HasIndex("IsBaby"); + + b.HasIndex("IsLegendary"); + + b.HasIndex("IsMythical"); + + b.HasIndex("Order"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("PokemonSpecies", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonTypeEntity", b => + { + b.Property("PokemonTypeId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("PokemonTypeId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("PokemonTypes", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonVarietyEntity", b => + { + b.Property("PokemonVarietyId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("BaseExperienceYield") + .HasColumnType("int"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Height") + .HasColumnType("float"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PokemonSpeciesId") + .HasColumnType("int"); + + b.Property("PrimaryTypeId") + .HasColumnType("int"); + + b.Property("SecondaryTypeId") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.Property("Weight") + .HasColumnType("float"); + + b.HasKey("PokemonVarietyId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("IsDefault"); + + b.HasIndex("Order"); + + b.HasIndex("PokemonSpeciesId"); + + b.HasIndex("PrimaryTypeId"); + + b.HasIndex("SecondaryTypeId"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("PokemonVarieties", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Property("RegionId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MainGenerationId") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("RegionId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Regions", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.ResourceEntity", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ResourceId")); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("SourceNormalized") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("ResourceId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("ContentType"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Source"); + + b.HasIndex("SourceNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Resources", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", "MainRegion") + .WithOne("MainGeneration") + .HasForeignKey("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", "GenerationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MainRegion"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", b => + { + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", "Generation") + .WithMany("Species") + .HasForeignKey("GenerationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Generation"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonVarietyEntity", b => + { + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", "Species") + .WithMany("Varieties") + .HasForeignKey("PokemonSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.PokemonTypeEntity", "PrimaryType") + .WithMany() + .HasForeignKey("PrimaryTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.PokemonTypeEntity", "SecondaryType") + .WithMany() + .HasForeignKey("SecondaryTypeId"); + + b.Navigation("PrimaryType"); + + b.Navigation("SecondaryType"); + + b.Navigation("Species"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.Navigation("Species"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", b => + { + b.Navigation("Varieties"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Navigation("MainGeneration"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/20231231153029_InitialMigration.cs b/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/20231231153029_InitialMigration.cs new file mode 100644 index 0000000..d0bc798 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/20231231153029_InitialMigration.cs @@ -0,0 +1,551 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.SqlServer.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PokemonTypes", + columns: table => new + { + PokemonTypeId = table.Column(type: "int", nullable: false), + UniqueName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + UniqueNameNormalized = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + DisplayName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + AggregateId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Version = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + UpdatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + UpdatedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PokemonTypes", x => x.PokemonTypeId); + }); + + migrationBuilder.CreateTable( + name: "Regions", + columns: table => new + { + RegionId = table.Column(type: "int", nullable: false), + UniqueName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + UniqueNameNormalized = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + DisplayName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + MainGenerationId = table.Column(type: "int", nullable: true), + AggregateId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Version = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + UpdatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + UpdatedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Regions", x => x.RegionId); + }); + + migrationBuilder.CreateTable( + name: "Resources", + columns: table => new + { + ResourceId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Source = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: false), + SourceNormalized = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: false), + ContentType = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + ContentText = table.Column(type: "nvarchar(max)", nullable: false), + AggregateId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Version = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + UpdatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + UpdatedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Resources", x => x.ResourceId); + }); + + migrationBuilder.CreateTable( + name: "Generations", + columns: table => new + { + GenerationId = table.Column(type: "int", nullable: false), + UniqueName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + UniqueNameNormalized = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + DisplayName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + MainRegionId = table.Column(type: "int", nullable: false), + AggregateId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Version = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + UpdatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + UpdatedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Generations", x => x.GenerationId); + table.ForeignKey( + name: "FK_Generations_Regions_GenerationId", + column: x => x.GenerationId, + principalTable: "Regions", + principalColumn: "RegionId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PokemonSpecies", + columns: table => new + { + PokemonSpeciesId = table.Column(type: "int", nullable: false), + GenerationId = table.Column(type: "int", nullable: false), + Order = table.Column(type: "int", nullable: false), + IsBaby = table.Column(type: "bit", nullable: false), + IsLegendary = table.Column(type: "bit", nullable: false), + IsMythical = table.Column(type: "bit", nullable: false), + HasGenderDifferences = table.Column(type: "bit", nullable: false), + CanSwitchForm = table.Column(type: "bit", nullable: false), + UniqueName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + UniqueNameNormalized = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + DisplayName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + Category = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + GenderRatio = table.Column(type: "float", nullable: true), + CatchRate = table.Column(type: "tinyint", nullable: false), + HatchTime = table.Column(type: "tinyint", nullable: false), + BaseFriendship = table.Column(type: "tinyint", nullable: false), + AggregateId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Version = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + UpdatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + UpdatedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PokemonSpecies", x => x.PokemonSpeciesId); + table.ForeignKey( + name: "FK_PokemonSpecies_Generations_GenerationId", + column: x => x.GenerationId, + principalTable: "Generations", + principalColumn: "GenerationId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PokemonVarieties", + columns: table => new + { + PokemonVarietyId = table.Column(type: "int", nullable: false), + Order = table.Column(type: "int", nullable: false), + UniqueName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + UniqueNameNormalized = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + PrimaryTypeId = table.Column(type: "int", nullable: false), + SecondaryTypeId = table.Column(type: "int", nullable: true), + Height = table.Column(type: "float", nullable: false), + Weight = table.Column(type: "float", nullable: false), + BaseExperienceYield = table.Column(type: "int", nullable: false), + PokemonSpeciesId = table.Column(type: "int", nullable: false), + IsDefault = table.Column(type: "bit", nullable: false), + AggregateId = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + Version = table.Column(type: "bigint", nullable: false), + CreatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + CreatedOn = table.Column(type: "datetime2", nullable: false), + UpdatedBy = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: false), + UpdatedOn = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PokemonVarieties", x => x.PokemonVarietyId); + table.ForeignKey( + name: "FK_PokemonVarieties_PokemonSpecies_PokemonSpeciesId", + column: x => x.PokemonSpeciesId, + principalTable: "PokemonSpecies", + principalColumn: "PokemonSpeciesId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PokemonVarieties_PokemonTypes_PrimaryTypeId", + column: x => x.PrimaryTypeId, + principalTable: "PokemonTypes", + principalColumn: "PokemonTypeId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PokemonVarieties_PokemonTypes_SecondaryTypeId", + column: x => x.SecondaryTypeId, + principalTable: "PokemonTypes", + principalColumn: "PokemonTypeId"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Generations_AggregateId", + table: "Generations", + column: "AggregateId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Generations_CreatedBy", + table: "Generations", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Generations_CreatedOn", + table: "Generations", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Generations_DisplayName", + table: "Generations", + column: "DisplayName"); + + migrationBuilder.CreateIndex( + name: "IX_Generations_UniqueName", + table: "Generations", + column: "UniqueName"); + + migrationBuilder.CreateIndex( + name: "IX_Generations_UniqueNameNormalized", + table: "Generations", + column: "UniqueNameNormalized", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Generations_UpdatedBy", + table: "Generations", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Generations_UpdatedOn", + table: "Generations", + column: "UpdatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Generations_Version", + table: "Generations", + column: "Version"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_AggregateId", + table: "PokemonSpecies", + column: "AggregateId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_CanSwitchForm", + table: "PokemonSpecies", + column: "CanSwitchForm"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_Category", + table: "PokemonSpecies", + column: "Category"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_CreatedBy", + table: "PokemonSpecies", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_CreatedOn", + table: "PokemonSpecies", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_DisplayName", + table: "PokemonSpecies", + column: "DisplayName"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_GenerationId", + table: "PokemonSpecies", + column: "GenerationId"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_HasGenderDifferences", + table: "PokemonSpecies", + column: "HasGenderDifferences"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_IsBaby", + table: "PokemonSpecies", + column: "IsBaby"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_IsLegendary", + table: "PokemonSpecies", + column: "IsLegendary"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_IsMythical", + table: "PokemonSpecies", + column: "IsMythical"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_Order", + table: "PokemonSpecies", + column: "Order"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_UniqueName", + table: "PokemonSpecies", + column: "UniqueName"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_UniqueNameNormalized", + table: "PokemonSpecies", + column: "UniqueNameNormalized", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_UpdatedBy", + table: "PokemonSpecies", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_UpdatedOn", + table: "PokemonSpecies", + column: "UpdatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonSpecies_Version", + table: "PokemonSpecies", + column: "Version"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_AggregateId", + table: "PokemonTypes", + column: "AggregateId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_CreatedBy", + table: "PokemonTypes", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_CreatedOn", + table: "PokemonTypes", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_DisplayName", + table: "PokemonTypes", + column: "DisplayName"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_UniqueName", + table: "PokemonTypes", + column: "UniqueName"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_UniqueNameNormalized", + table: "PokemonTypes", + column: "UniqueNameNormalized", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_UpdatedBy", + table: "PokemonTypes", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_UpdatedOn", + table: "PokemonTypes", + column: "UpdatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonTypes_Version", + table: "PokemonTypes", + column: "Version"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_AggregateId", + table: "PokemonVarieties", + column: "AggregateId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_CreatedBy", + table: "PokemonVarieties", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_CreatedOn", + table: "PokemonVarieties", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_IsDefault", + table: "PokemonVarieties", + column: "IsDefault"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_Order", + table: "PokemonVarieties", + column: "Order"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_PokemonSpeciesId", + table: "PokemonVarieties", + column: "PokemonSpeciesId"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_PrimaryTypeId", + table: "PokemonVarieties", + column: "PrimaryTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_SecondaryTypeId", + table: "PokemonVarieties", + column: "SecondaryTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_UniqueName", + table: "PokemonVarieties", + column: "UniqueName"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_UniqueNameNormalized", + table: "PokemonVarieties", + column: "UniqueNameNormalized", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_UpdatedBy", + table: "PokemonVarieties", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_UpdatedOn", + table: "PokemonVarieties", + column: "UpdatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_PokemonVarieties_Version", + table: "PokemonVarieties", + column: "Version"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_AggregateId", + table: "Regions", + column: "AggregateId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Regions_CreatedBy", + table: "Regions", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_CreatedOn", + table: "Regions", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_DisplayName", + table: "Regions", + column: "DisplayName"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_UniqueName", + table: "Regions", + column: "UniqueName"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_UniqueNameNormalized", + table: "Regions", + column: "UniqueNameNormalized", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Regions_UpdatedBy", + table: "Regions", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_UpdatedOn", + table: "Regions", + column: "UpdatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_Version", + table: "Regions", + column: "Version"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_AggregateId", + table: "Resources", + column: "AggregateId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Resources_ContentType", + table: "Resources", + column: "ContentType"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_CreatedBy", + table: "Resources", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_CreatedOn", + table: "Resources", + column: "CreatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_Source", + table: "Resources", + column: "Source"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_SourceNormalized", + table: "Resources", + column: "SourceNormalized", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Resources_UpdatedBy", + table: "Resources", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_UpdatedOn", + table: "Resources", + column: "UpdatedOn"); + + migrationBuilder.CreateIndex( + name: "IX_Resources_Version", + table: "Resources", + column: "Version"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PokemonVarieties"); + + migrationBuilder.DropTable( + name: "Resources"); + + migrationBuilder.DropTable( + name: "PokemonSpecies"); + + migrationBuilder.DropTable( + name: "PokemonTypes"); + + migrationBuilder.DropTable( + name: "Generations"); + + migrationBuilder.DropTable( + name: "Regions"); + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/PokemonContextModelSnapshot.cs b/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/PokemonContextModelSnapshot.cs new file mode 100644 index 0000000..14e7d0e --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.SqlServer/Migrations/PokemonContextModelSnapshot.cs @@ -0,0 +1,592 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PokeData.EntityFrameworkCore.Relational; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.SqlServer.Migrations +{ + [DbContext(typeof(PokemonContext))] + partial class PokemonContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.Property("GenerationId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MainRegionId") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("GenerationId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Generations", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", b => + { + b.Property("PokemonSpeciesId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("BaseFriendship") + .HasColumnType("tinyint"); + + b.Property("CanSwitchForm") + .HasColumnType("bit"); + + b.Property("CatchRate") + .HasColumnType("tinyint"); + + b.Property("Category") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("GenderRatio") + .HasColumnType("float"); + + b.Property("GenerationId") + .HasColumnType("int"); + + b.Property("HasGenderDifferences") + .HasColumnType("bit"); + + b.Property("HatchTime") + .HasColumnType("tinyint"); + + b.Property("IsBaby") + .HasColumnType("bit"); + + b.Property("IsLegendary") + .HasColumnType("bit"); + + b.Property("IsMythical") + .HasColumnType("bit"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("PokemonSpeciesId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CanSwitchForm"); + + b.HasIndex("Category"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("GenerationId"); + + b.HasIndex("HasGenderDifferences"); + + b.HasIndex("IsBaby"); + + b.HasIndex("IsLegendary"); + + b.HasIndex("IsMythical"); + + b.HasIndex("Order"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("PokemonSpecies", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonTypeEntity", b => + { + b.Property("PokemonTypeId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("PokemonTypeId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("PokemonTypes", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonVarietyEntity", b => + { + b.Property("PokemonVarietyId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("BaseExperienceYield") + .HasColumnType("int"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Height") + .HasColumnType("float"); + + b.Property("IsDefault") + .HasColumnType("bit"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PokemonSpeciesId") + .HasColumnType("int"); + + b.Property("PrimaryTypeId") + .HasColumnType("int"); + + b.Property("SecondaryTypeId") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.Property("Weight") + .HasColumnType("float"); + + b.HasKey("PokemonVarietyId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("IsDefault"); + + b.HasIndex("Order"); + + b.HasIndex("PokemonSpeciesId"); + + b.HasIndex("PrimaryTypeId"); + + b.HasIndex("SecondaryTypeId"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("PokemonVarieties", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Property("RegionId") + .HasColumnType("int"); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MainGenerationId") + .HasColumnType("int"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UniqueNameNormalized") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("RegionId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName"); + + b.HasIndex("UniqueNameNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Regions", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.ResourceEntity", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ResourceId")); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ContentText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("CreatedOn") + .HasColumnType("datetime2"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("SourceNormalized") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("ResourceId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("ContentType"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Source"); + + b.HasIndex("SourceNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Resources", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", "MainRegion") + .WithOne("MainGeneration") + .HasForeignKey("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", "GenerationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MainRegion"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", b => + { + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", "Generation") + .WithMany("Species") + .HasForeignKey("GenerationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Generation"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonVarietyEntity", b => + { + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", "Species") + .WithMany("Varieties") + .HasForeignKey("PokemonSpeciesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.PokemonTypeEntity", "PrimaryType") + .WithMany() + .HasForeignKey("PrimaryTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PokeData.EntityFrameworkCore.Relational.Entities.PokemonTypeEntity", "SecondaryType") + .WithMany() + .HasForeignKey("SecondaryTypeId"); + + b.Navigation("PrimaryType"); + + b.Navigation("SecondaryType"); + + b.Navigation("Species"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.Navigation("Species"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.PokemonSpeciesEntity", b => + { + b.Navigation("Varieties"); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Navigation("MainGeneration"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.SqlServer/PokeData.EntityFrameworkCore.SqlServer.csproj b/src/PokeData.EntityFrameworkCore.SqlServer/PokeData.EntityFrameworkCore.SqlServer.csproj new file mode 100644 index 0000000..49ed374 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.SqlServer/PokeData.EntityFrameworkCore.SqlServer.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + Nullable + + + + + + + + + + + diff --git a/src/PokeData.EntityFrameworkCore.SqlServer/README.md b/src/PokeData.EntityFrameworkCore.SqlServer/README.md new file mode 100644 index 0000000..154fe32 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.SqlServer/README.md @@ -0,0 +1,31 @@ +# PokeData.EntityFrameworkCore.SqlServer + +Provides an implementation of a relational event store to be used with PokéData management system, Entity Framework Core and Microsoft SQL Server. + +## Migrations + +This project is setup to use migrations. All the commands below must be executed in the solution directory. + +### Create a migration + +To create a new migration, execute the following command. Do not forget to provide a migration name! + +```sh +dotnet ef migrations add --context PokemonContext --project src/PokeData.EntityFrameworkCore.SqlServer --startup-project src/PokeData +``` + +### Remove a migration + +To remove the latest unapplied migration, execute the following command. + +```sh +dotnet ef migrations remove --context PokemonContext --project src/PokeData.EntityFrameworkCore.SqlServer --startup-project src/PokeData +``` + +### Generate a script + +To generate a script, execute the following command. Do not forget to provide a source migration name! + +```sh +dotnet ef migrations script --context PokemonContext --project src/PokeData.EntityFrameworkCore.SqlServer --startup-project src/PokeData +``` diff --git a/src/PokeData.Infrastructure.PokeApiClient/DependencyInjectionExtensions.cs b/src/PokeData.Infrastructure.PokeApiClient/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..f89307e --- /dev/null +++ b/src/PokeData.Infrastructure.PokeApiClient/DependencyInjectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using PokeData.Application; + +namespace PokeData.Infrastructure.PokeApiClient; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddPokeDataWithPokeApiClient(this IServiceCollection services) + { + return services + .AddHttpClient() + .AddPokeDataInfrastructure() + .AddTransient(); + } +} diff --git a/src/PokeData.Infrastructure.PokeApiClient/HttpFailureException.cs b/src/PokeData.Infrastructure.PokeApiClient/HttpFailureException.cs new file mode 100644 index 0000000..5c372f9 --- /dev/null +++ b/src/PokeData.Infrastructure.PokeApiClient/HttpFailureException.cs @@ -0,0 +1,16 @@ +namespace PokeData.Infrastructure.PokeApiClient; + +public class HttpFailureException : Exception +{ + public HttpResponseDetail Detail + { + get => (HttpResponseDetail)Data[nameof(Detail)]!; + private set => Data[nameof(Detail)] = value; + } + + public HttpFailureException(HttpResponseDetail detail, Exception innerException) + : base("The remote API did not return a success status code.", innerException) + { + Detail = detail; + } +} diff --git a/src/PokeData.Infrastructure.PokeApiClient/HttpResponseDetail.cs b/src/PokeData.Infrastructure.PokeApiClient/HttpResponseDetail.cs new file mode 100644 index 0000000..6ff8fc6 --- /dev/null +++ b/src/PokeData.Infrastructure.PokeApiClient/HttpResponseDetail.cs @@ -0,0 +1,12 @@ +namespace PokeData.Infrastructure.PokeApiClient; + +public record HttpResponseDetail +{ + public string? Content { get; set; } + public string? ReasonPhrase { get; set; } + public string? RequestMethod { get; set; } + public string? RequestUri { get; set; } + public int StatusCode { get; set; } + public string StatusText { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; +} diff --git a/src/PokeData.Infrastructure.PokeApiClient/HttpResponseMessageExtensions.cs b/src/PokeData.Infrastructure.PokeApiClient/HttpResponseMessageExtensions.cs new file mode 100644 index 0000000..f6d78d1 --- /dev/null +++ b/src/PokeData.Infrastructure.PokeApiClient/HttpResponseMessageExtensions.cs @@ -0,0 +1,33 @@ +namespace PokeData.Infrastructure.PokeApiClient; + +internal static class HttpResponseMessageExtensions +{ + public static async Task DetailAsync(this HttpResponseMessage response, CancellationToken cancellationToken) + { + string? content = null; + try + { + content = await response.Content.ReadAsStringAsync(cancellationToken); + } + catch (Exception) + { + } + + HttpResponseDetail detail = new() + { + Content = content, + ReasonPhrase = response.ReasonPhrase, + StatusCode = (int)response.StatusCode, + StatusText = response.StatusCode.ToString(), + Version = response.Version.ToString() + }; + + if (response.RequestMessage != null) + { + detail.RequestMethod = response.RequestMessage.ToString(); + detail.RequestUri = response.RequestMessage.RequestUri?.ToString(); + } + + return detail; + } +} diff --git a/src/PokeData.ETL/Models/APIResource.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/APIResource.cs similarity index 73% rename from src/PokeData.ETL/Models/APIResource.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/APIResource.cs index f2205fe..eb78e05 100644 --- a/src/PokeData.ETL/Models/APIResource.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/APIResource.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; internal record APIResource { diff --git a/src/PokeData.ETL/Models/Generation.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/GenerationModel.cs similarity index 84% rename from src/PokeData.ETL/Models/Generation.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/GenerationModel.cs index eb84b77..5572a31 100644 --- a/src/PokeData.ETL/Models/Generation.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/GenerationModel.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; -internal record Generation +internal record GenerationModel { [JsonPropertyName("id")] public int Id { get; set; } diff --git a/src/PokeData.ETL/Models/Genus.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/Genus.cs similarity index 80% rename from src/PokeData.ETL/Models/Genus.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/Genus.cs index ac5d553..c5f9aa6 100644 --- a/src/PokeData.ETL/Models/Genus.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/Genus.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; internal record Genus { diff --git a/src/PokeData.ETL/Models/Name.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/Name.cs similarity index 80% rename from src/PokeData.ETL/Models/Name.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/Name.cs index e9e7744..7a7966c 100644 --- a/src/PokeData.ETL/Models/Name.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/Name.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; internal record Name { diff --git a/src/PokeData.ETL/Models/NamedAPIResource.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/NamedAPIResource.cs similarity index 75% rename from src/PokeData.ETL/Models/NamedAPIResource.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/NamedAPIResource.cs index bcec5f2..a81e2d4 100644 --- a/src/PokeData.ETL/Models/NamedAPIResource.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/NamedAPIResource.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; internal record NamedAPIResource : APIResource { diff --git a/src/PokeData.ETL/Models/Pokemon.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/Pokemon.cs similarity index 89% rename from src/PokeData.ETL/Models/Pokemon.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/Pokemon.cs index 53a1133..a3bf448 100644 --- a/src/PokeData.ETL/Models/Pokemon.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/Pokemon.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; internal class Pokemon { @@ -40,5 +40,5 @@ internal class Pokemon // TODO(fpion): stats [JsonPropertyName("types")] - public List Types { get; set; } = []; + public List Types { get; set; } = []; } diff --git a/src/PokeData.ETL/Models/PokemonSpecies.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/PokemonSpeciesModel.cs similarity index 94% rename from src/PokeData.ETL/Models/PokemonSpecies.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/PokemonSpeciesModel.cs index b57cdbf..9fdfc58 100644 --- a/src/PokeData.ETL/Models/PokemonSpecies.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/PokemonSpeciesModel.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; -internal class PokemonSpecies +internal class PokemonSpeciesModel { [JsonPropertyName("id")] public int Id { get; set; } diff --git a/src/PokeData.ETL/Models/PokemonSpeciesVariety.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/PokemonSpeciesVariety.cs similarity index 81% rename from src/PokeData.ETL/Models/PokemonSpeciesVariety.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/PokemonSpeciesVariety.cs index 8b5c02d..c1589db 100644 --- a/src/PokeData.ETL/Models/PokemonSpeciesVariety.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/PokemonSpeciesVariety.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; internal record PokemonSpeciesVariety { diff --git a/src/PokeData.ETL/Models/PokemonType.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/PokemonTypeModel.cs similarity index 67% rename from src/PokeData.ETL/Models/PokemonType.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/PokemonTypeModel.cs index 37aa69b..1c51d2e 100644 --- a/src/PokeData.ETL/Models/PokemonType.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/PokemonTypeModel.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; -internal record PokemonType +internal record PokemonTypeModel { [JsonPropertyName("slot")] public int Slot { get; set; } diff --git a/src/PokeData.ETL/Models/Region.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/RegionModel.cs similarity index 88% rename from src/PokeData.ETL/Models/Region.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/RegionModel.cs index 66f6b85..d26b558 100644 --- a/src/PokeData.ETL/Models/Region.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/RegionModel.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; -internal class Region +internal class RegionModel { [JsonPropertyName("id")] public int Id { get; set; } diff --git a/src/PokeData.ETL/Models/Type.cs b/src/PokeData.Infrastructure.PokeApiClient/Models/TypeModel.cs similarity index 87% rename from src/PokeData.ETL/Models/Type.cs rename to src/PokeData.Infrastructure.PokeApiClient/Models/TypeModel.cs index 3e74fa8..eca1f4a 100644 --- a/src/PokeData.ETL/Models/Type.cs +++ b/src/PokeData.Infrastructure.PokeApiClient/Models/TypeModel.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -namespace PokeData.ETL.Models; +namespace PokeData.Infrastructure.PokeApiClient.Models; -internal class Type +internal class TypeModel { [JsonPropertyName("id")] public int Id { get; set; } diff --git a/src/PokeData.Infrastructure.PokeApiClient/PokeData.Infrastructure.PokeApiClient.csproj b/src/PokeData.Infrastructure.PokeApiClient/PokeData.Infrastructure.PokeApiClient.csproj new file mode 100644 index 0000000..6feb6a3 --- /dev/null +++ b/src/PokeData.Infrastructure.PokeApiClient/PokeData.Infrastructure.PokeApiClient.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + Nullable + + + + + + + + + + + + + + + + diff --git a/src/PokeData.Infrastructure.PokeApiClient/ResourceService.cs b/src/PokeData.Infrastructure.PokeApiClient/ResourceService.cs new file mode 100644 index 0000000..3c08342 --- /dev/null +++ b/src/PokeData.Infrastructure.PokeApiClient/ResourceService.cs @@ -0,0 +1,189 @@ +using PokeData.Application; +using PokeData.Application.Caching; +using PokeData.Domain; +using PokeData.Domain.Generations; +using PokeData.Domain.Regions; +using PokeData.Domain.Resources; +using PokeData.Domain.Species; +using PokeData.Infrastructure.PokeApiClient.Models; + +namespace PokeData.Infrastructure.PokeApiClient; + +internal class ResourceService : IResourceService +{ + private const string LanguageName = "en"; + + private readonly Random _random = new(); + + private readonly ICacheService _cache; + private readonly HttpClient _client; + private readonly IResourceRepository _repository; + + public ResourceService(ICacheService cache, HttpClient client, IResourceRepository repository) + { + _cache = cache; + _client = client; + _repository = repository; + } + + public async Task GetSpeciesAsync(string id, CancellationToken cancellationToken) + { + Uri source = new($"https://pokeapi.co/api/v2/pokemon-species/{id}/"); + PokemonSpeciesModel? speciesModel = await ExtractAsync(source, cancellationToken); + if (speciesModel == null) + { + return null; + } + + Generation? generation = null; + if (speciesModel.Generation != null) + { + GenerationModel? generationModel = await ExtractAsync(speciesModel.Generation, cancellationToken); + if (generationModel != null) + { + generation = new() + { + Number = (byte)generationModel.Id, + UniqueName = generationModel.Name, + DisplayName = generationModel.Names.SingleOrDefault(name => name.Language?.Name == LanguageName)?.Value + }; + + if (generationModel.MainRegion != null) + { + RegionModel? regionModel = await ExtractAsync(generationModel.MainRegion, cancellationToken); + if (regionModel != null) + { + Region region = new() + { + Number = (byte)regionModel.Id, + UniqueName = regionModel.Name, + DisplayName = regionModel.Names.SingleOrDefault(name => name.Language?.Name == LanguageName)?.Value, + MainGeneration = generation + }; + generation.MainRegion = region; + } + } + } + } + + PokemonSpecies species = new() + { + Number = (ushort)speciesModel.Id, + Order = (ushort)speciesModel.Order, + IsBaby = speciesModel.IsBaby, + IsLegendary = speciesModel.IsLegendary, + IsMythical = speciesModel.IsMythical, + HasGenderDifferences = speciesModel.HasGenderDifferences, + CanSwitchForm = speciesModel.FormsSwitchable, + UniqueName = speciesModel.Name, + DisplayName = speciesModel.Names.SingleOrDefault(name => name.Language?.Name == LanguageName)?.Value, + Category = speciesModel.Genera.SingleOrDefault(genus => genus.Language?.Name == LanguageName)?.Value, + GenderRatio = speciesModel.GenderRate == -1 ? null : (1 - speciesModel.GenderRate / 8.0), + CatchRate = (byte?)speciesModel.CaptureRate ?? 0, + HatchTime = (byte?)speciesModel.HatchCounter ?? 0, + BaseFriendship = (byte?)speciesModel.BaseHappiness ?? 0, + Generation = generation, + Varieties = new List(capacity: speciesModel.Varieties.Count) + }; + + foreach (PokemonSpeciesVariety speciesVariety in speciesModel.Varieties) + { + if (speciesVariety.Pokemon != null) + { + Pokemon? pokemon = await ExtractAsync(speciesVariety.Pokemon, cancellationToken); + if (pokemon != null) + { + PokemonVariety variety = new() + { + Number = (ushort)pokemon.Id, + Order = (ushort)pokemon.Order, + UniqueName = pokemon.Name, + Height = pokemon.Height / 10.0, + Weight = pokemon.Weight / 10.0, + BaseExperienceYield = (ushort?)pokemon.BaseExperience ?? 0, + Species = species, + IsDefault = speciesVariety.IsDefault + }; + species.Varieties.Add(variety); + + foreach (PokemonTypeModel item in pokemon.Types) + { + if (item.Type != null) + { + TypeModel? typeModel = await ExtractAsync(item.Type, cancellationToken); + if (typeModel != null) + { + PokemonType type = new() + { + Number = (byte)typeModel.Id, + UniqueName = typeModel.Name, + DisplayName = typeModel.Names.SingleOrDefault(name => name.Language?.Name == LanguageName)?.Value + }; + + switch (item.Slot) + { + case 1: + variety.PrimaryType = type; + break; + case 2: + variety.SecondaryType = type; + break; + default: + throw new NotSupportedException($"The Pokémon type slot '{item.Slot}' is not supported."); + } + } + } + } + } + } + } + + return species; + } + + private async Task ExtractAsync(APIResource resource, CancellationToken cancellationToken) + => await ExtractAsync(new Uri(resource.Url), cancellationToken); + private async Task ExtractAsync(Uri source, CancellationToken cancellationToken) + { + Resource? resource = _cache.GetResource(source) ?? await _repository.GetAsync(source, cancellationToken); + if (resource != null) + { + _cache.SetResource(resource); + return Deserialize(resource.Content); + } + + int millisecondsDelay = _random.Next(50, 500 + 1); + await Task.Delay(millisecondsDelay, cancellationToken); + + using HttpRequestMessage request = new(HttpMethod.Get, source); + using HttpResponseMessage response = await _client.SendAsync(request, cancellationToken); + try + { + response.EnsureSuccessStatusCode(); + } + catch (Exception innerException) + { + HttpResponseDetail detail = await response.DetailAsync(cancellationToken); + throw new HttpFailureException(detail, innerException); + } + + string type = response.Content.Headers.ContentType?.MediaType ?? MediaTypeNames.Application.Json; + string text = await response.Content.ReadAsStringAsync(cancellationToken); + + Content content = new(type, text); + resource = new(source, content); + + await _repository.SaveAsync(resource, cancellationToken); + + _cache.SetResource(resource); + return Deserialize(content); + } + private static T? Deserialize(Content content) + { + return content.Type switch + { + MediaTypeNames.Application.Json => JsonSerializer.Deserialize(content.Text), + _ => throw new NotSupportedException($"The content type '{content.Type}' is not supported."), + }; + } +} diff --git a/src/PokeData.Infrastructure/Caching/CacheService.cs b/src/PokeData.Infrastructure/Caching/CacheService.cs new file mode 100644 index 0000000..3e8fc71 --- /dev/null +++ b/src/PokeData.Infrastructure/Caching/CacheService.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Caching.Memory; +using PokeData.Application.Caching; +using PokeData.Domain.Resources; +using PokeData.Infrastructure.Settings; + +namespace PokeData.Infrastructure.Caching; + +internal class CacheService : ICacheService +{ + private readonly IMemoryCache _cache; + private readonly CachingSettings _settings; + + public CacheService(IMemoryCache cache, CachingSettings settings) + { + _cache = cache; + _settings = settings; + } + + public Resource? GetResource(Uri source) => GetItem(GetResourceKey(source)); + public void SetResource(Resource resource) + { + TimeSpan? expiration = _settings.ResourceLifetimeSeconds > 0 ? TimeSpan.FromSeconds(_settings.ResourceLifetimeSeconds) : null; + SetItem(GetResourceKey(resource.Source), resource, expiration); + } + private static string GetResourceKey(Uri source) => $"Resource.Source|{source.ToString().ToUpper()}"; + + private T? GetItem(object key) => _cache.TryGetValue(key, out object? value) ? (T?)value : default; + private void SetItem(object key, T value, TimeSpan? expiration = null) + { + if (expiration.HasValue) + { + _cache.Set(key, value, expiration.Value); + } + else + { + _cache.Set(key, value); + } + } +} diff --git a/src/PokeData.Infrastructure/Commands/InitializeCachingCommand.cs b/src/PokeData.Infrastructure/Commands/InitializeCachingCommand.cs new file mode 100644 index 0000000..326b661 --- /dev/null +++ b/src/PokeData.Infrastructure/Commands/InitializeCachingCommand.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace PokeData.Infrastructure.Commands; + +public record InitializeCachingCommand : INotification; diff --git a/src/PokeData.Infrastructure/Commands/InitializeCachingCommandHandler.cs b/src/PokeData.Infrastructure/Commands/InitializeCachingCommandHandler.cs new file mode 100644 index 0000000..67185be --- /dev/null +++ b/src/PokeData.Infrastructure/Commands/InitializeCachingCommandHandler.cs @@ -0,0 +1,32 @@ +using MediatR; +using PokeData.Application.Caching; +using PokeData.Domain.Resources; +using PokeData.Infrastructure.Settings; + +namespace PokeData.Infrastructure.Commands; + +internal class InitializeCachingCommandHandler : INotificationHandler +{ + private readonly ICacheService _cacheService; + private readonly CachingSettings _cachingSettings; + private readonly IResourceRepository _resourceRepository; + + public InitializeCachingCommandHandler(ICacheService cacheService, CachingSettings cachingSettings, IResourceRepository resourceRepository) + { + _cacheService = cacheService; + _cachingSettings = cachingSettings; + _resourceRepository = resourceRepository; + } + + public async Task Handle(InitializeCachingCommand _, CancellationToken cancellationToken) + { + if (_cachingSettings.LoadResourcesOnStartup) + { + IEnumerable resources = await _resourceRepository.GetAsync(cancellationToken); + foreach (Resource resource in resources) + { + _cacheService.SetResource(resource); + } + } + } +} diff --git a/src/PokeData.Infrastructure/Commands/InitializeDatabaseCommand.cs b/src/PokeData.Infrastructure/Commands/InitializeDatabaseCommand.cs new file mode 100644 index 0000000..7f8c903 --- /dev/null +++ b/src/PokeData.Infrastructure/Commands/InitializeDatabaseCommand.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace PokeData.Infrastructure.Commands; + +public record InitializeDatabaseCommand : INotification; diff --git a/src/PokeData.Infrastructure/DependencyInjectionExtensions.cs b/src/PokeData.Infrastructure/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..738f93d --- /dev/null +++ b/src/PokeData.Infrastructure/DependencyInjectionExtensions.cs @@ -0,0 +1,27 @@ +using Logitar.EventSourcing.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using PokeData.Application; +using PokeData.Application.Caching; +using PokeData.Infrastructure.Caching; +using PokeData.Infrastructure.Settings; + +namespace PokeData.Infrastructure; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddPokeDataInfrastructure(this IServiceCollection services) + { + return services + .AddLogitarEventSourcingInfrastructure() + .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) + .AddPokeDataApplication() + .AddSingleton(serviceProvider => + { + IConfiguration configuration = serviceProvider.GetRequiredService(); + return configuration.GetSection("Caching").Get() ?? new(); + }) + .AddSingleton() + .AddTransient(); + } +} diff --git a/src/PokeData.Infrastructure/EventBus.cs b/src/PokeData.Infrastructure/EventBus.cs new file mode 100644 index 0000000..d666db5 --- /dev/null +++ b/src/PokeData.Infrastructure/EventBus.cs @@ -0,0 +1,20 @@ +using Logitar.EventSourcing; +using Logitar.EventSourcing.Infrastructure; +using MediatR; + +namespace PokeData.Infrastructure; + +internal class EventBus : IEventBus +{ + private readonly IPublisher _publisher; + + public EventBus(IPublisher publisher) + { + _publisher = publisher; + } + + public async Task PublishAsync(DomainEvent @event, CancellationToken cancellationToken) + { + await _publisher.Publish(@event, cancellationToken); + } +} diff --git a/src/PokeData.Infrastructure/PokeData.Infrastructure.csproj b/src/PokeData.Infrastructure/PokeData.Infrastructure.csproj new file mode 100644 index 0000000..40cecb1 --- /dev/null +++ b/src/PokeData.Infrastructure/PokeData.Infrastructure.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + Nullable + + + + + + + + + + + + + + + + diff --git a/src/PokeData.Infrastructure/Settings/CachingSettings.cs b/src/PokeData.Infrastructure/Settings/CachingSettings.cs new file mode 100644 index 0000000..742af9e --- /dev/null +++ b/src/PokeData.Infrastructure/Settings/CachingSettings.cs @@ -0,0 +1,7 @@ +namespace PokeData.Infrastructure.Settings; + +internal record CachingSettings +{ + public bool LoadResourcesOnStartup { get; set; } + public int ResourceLifetimeSeconds { get; set; } +} diff --git a/src/PokeData/Dockerfile b/src/PokeData/Dockerfile new file mode 100644 index 0000000..0948236 --- /dev/null +++ b/src/PokeData/Dockerfile @@ -0,0 +1,25 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["src/PokeData/PokeData.csproj", "src/PokeData/"] +RUN dotnet restore "./src/PokeData/./PokeData.csproj" +COPY . . +WORKDIR "/src/src/PokeData" +RUN dotnet build "./PokeData.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./PokeData.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PokeData.dll"] \ No newline at end of file diff --git a/src/PokeData/PokeData.csproj b/src/PokeData/PokeData.csproj new file mode 100644 index 0000000..eb88442 --- /dev/null +++ b/src/PokeData/PokeData.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + Nullable + 7ea9123e-e944-4437-8c3a-6b7351ac67bf + Linux + ..\.. + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/PokeData/PokeData.http b/src/PokeData/PokeData.http new file mode 100644 index 0000000..9205a73 --- /dev/null +++ b/src/PokeData/PokeData.http @@ -0,0 +1,6 @@ +@PokeData_HostAddress = http://localhost:5200 + +GET {{PokeData_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/PokeData/Program.cs b/src/PokeData/Program.cs new file mode 100644 index 0000000..d85bd1c --- /dev/null +++ b/src/PokeData/Program.cs @@ -0,0 +1,25 @@ +using MediatR; +using PokeData.Infrastructure.Commands; + +namespace PokeData; + +public class Program +{ + public static async Task Main(string[] args) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + + Startup startup = new(builder.Configuration); + startup.ConfigureServices(builder.Services); + + WebApplication application = builder.Build(); + + startup.Configure(application); + + using IServiceScope scope = application.Services.CreateScope(); + IPublisher publisher = scope.ServiceProvider.GetRequiredService(); + await publisher.Publish(new InitializeDatabaseCommand()); + + application.Run(); + } +} diff --git a/src/PokeData/Properties/launchSettings.json b/src/PokeData/Properties/launchSettings.json new file mode 100644 index 0000000..40916e4 --- /dev/null +++ b/src/PokeData/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5200" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7084;http://localhost:5200" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:42290", + "sslPort": 44363 + } + } +} \ No newline at end of file diff --git a/src/PokeData/Startup.cs b/src/PokeData/Startup.cs new file mode 100644 index 0000000..c19bccf --- /dev/null +++ b/src/PokeData/Startup.cs @@ -0,0 +1,51 @@ +using PokeData.EntityFrameworkCore.SqlServer; +using PokeData.Infrastructure.PokeApiClient; + +namespace PokeData; + +internal class Startup : StartupBase +{ + private readonly IConfiguration _configuration; + private readonly bool _enableOpenApi; + + public Startup(IConfiguration configuration) + { + _configuration = configuration; + _enableOpenApi = configuration.GetValue("EnableOpenApi"); + } + + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + + services.AddControllers(); + + if (_enableOpenApi) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + } + + services.AddMemoryCache(); + services.AddPokeDataWithEntityFrameworkCoreSqlServer(_configuration); + services.AddPokeDataWithPokeApiClient(); + } + + public override void Configure(IApplicationBuilder builder) + { + if (_enableOpenApi) + { + builder.UseSwagger(); + builder.UseSwaggerUI(); + } + + builder.UseHttpsRedirection(); + builder.UseAuthentication(); + builder.UseAuthorization(); + + if (builder is WebApplication application) + { + application.MapControllers(); + } + } +} diff --git a/src/PokeData/appsettings.Development.json b/src/PokeData/appsettings.Development.json new file mode 100644 index 0000000..1df1606 --- /dev/null +++ b/src/PokeData/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "EnableMigrations": true, + "EnableOpenApi": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "SQLCONNSTR_Pokemon": "Server=host.docker.internal,27945;Database=Pokemon;User Id=SA;Password=m7tPnE6dB5TQxYCW;Persist Security Info=False;Encrypt=False;" +} diff --git a/src/PokeData/appsettings.json b/src/PokeData/appsettings.json new file mode 100644 index 0000000..5d8ba2e --- /dev/null +++ b/src/PokeData/appsettings.json @@ -0,0 +1,9 @@ +{ + "AllowedHosts": "*", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +}