From 2caca357262bfb8f91f536519bae7af4329ac249 Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Tue, 22 Mar 2022 04:56:18 -0300 Subject: [PATCH 01/21] Update appveyor.yml --- appveyor.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 0d4876af..23a80e66 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,10 +1,14 @@ version: 1.0.{build} -image: Visual Studio 2019 +image: Visual Studio 2022 dotnet_csproj: patch: true file: '**\*.csproj' version: '{version}' + version_prefix: '{version}' package_version: '{version}' + assembly_version: '{version}' + file_version: '{version}' + informational_version: '{version}' before_build: - cmd: >- dotnet restore From a631fe648f5e8a191c165b3309b8b773f596db3f Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Tue, 22 Mar 2022 05:01:38 -0300 Subject: [PATCH 02/21] Updating to .NET 6 (#188) * Updating to .NET 6 * Update dotnet-core.yml --- .github/workflows/dotnet-core.yml | 2 +- README.md | 10 +- global.json | 4 +- .../Equinox.Application.csproj | 16 ++- .../Equinox.Domain.Core.csproj | 9 +- src/Equinox.Domain/Equinox.Domain.csproj | 10 +- .../Equinox.Infra.CrossCutting.Bus.csproj | 10 +- ...Equinox.Infra.CrossCutting.Identity.csproj | 14 +-- .../Equinox.Infra.CrossCutting.IoC.csproj | 2 +- .../Equinox.Infra.Data.csproj | 14 +-- .../Equinox.Services.Api.csproj | 47 ++++----- src/Equinox.Services.Api/Program.cs | 83 ++++++++++++---- src/Equinox.Services.Api/Startup.cs | 91 ----------------- src/Equinox.UI.Web/Equinox.UI.Web.csproj | 48 +++++---- src/Equinox.UI.Web/Program.cs | 90 +++++++++++++---- src/Equinox.UI.Web/Startup.cs | 99 ------------------- src/Equinox.UI.Web/Views/Home/Index.cshtml | 10 +- .../Views/Shared/_Layout.cshtml | 2 +- src/Equinox.UI.Web/wwwroot/images/banner1.svg | 2 +- 19 files changed, 225 insertions(+), 338 deletions(-) delete mode 100644 src/Equinox.Services.Api/Startup.cs delete mode 100644 src/Equinox.UI.Web/Startup.cs diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index 1a45a91c..e8608446 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.202 + dotnet-version: 6.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/README.md b/README.md index 6be9720b..4a327b75 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you liked the project or if Equinox helped you, please give a star ;) Check my online courses at [desenvolvedor.io](https://desenvolvedor.io) ## How to use: -- You will need the latest Visual Studio 2019 and the latest .NET Core SDK. +- You will need the latest Visual Studio 2022 and the latest .NET Core SDK. - ***Please check if you have installed the same runtime version (SDK) described in global.json*** - The latest SDK and tools can be downloaded from https://dot.net/core. @@ -29,11 +29,11 @@ To know more about how to setup your enviroment visit the [Microsoft .NET Downlo ## Technologies implemented: -- ASP.NET 5.0 (with .NET Core 5.0) +- ASP.NET 6.0 - ASP.NET MVC Core - ASP.NET WebApi Core with JWT Bearer Authentication - ASP.NET Identity Core -- Entity Framework Core 5.0 +- Entity Framework Core 6.0 - .NET Core Native DI - AutoMapper - FluentValidator @@ -56,6 +56,10 @@ To know more about how to setup your enviroment visit the [Microsoft .NET Downlo ## News +**v1.8 - 03/22/2022** +- Migrated for .NET 6.0 +- All dependencies is up to date + **v1.7 - 04/06/2021** - Migrated for .NET 5.0 - All dependencies is up to date diff --git a/global.json b/global.json index 47473c4a..cbc6e731 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "projects": ["src", "test"], + "projects": ["src"], "sdk": { - "version": "^5.0.202" + "version": "^6.0.201" } } diff --git a/src/Equinox.Application/Equinox.Application.csproj b/src/Equinox.Application/Equinox.Application.csproj index 8e83b43f..3a5b59ed 100644 --- a/src/Equinox.Application/Equinox.Application.csproj +++ b/src/Equinox.Application/Equinox.Application.csproj @@ -1,16 +1,12 @@ - - + - net5.0 + net6.0 - - - + + - - + - - + \ No newline at end of file diff --git a/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj b/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj index a3b76027..8e37f651 100644 --- a/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj +++ b/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj @@ -1,11 +1,8 @@ - - net5.0 + net6.0 - - + - - + \ No newline at end of file diff --git a/src/Equinox.Domain/Equinox.Domain.csproj b/src/Equinox.Domain/Equinox.Domain.csproj index 9988d37b..472d8dfb 100644 --- a/src/Equinox.Domain/Equinox.Domain.csproj +++ b/src/Equinox.Domain/Equinox.Domain.csproj @@ -1,19 +1,15 @@ - - net5.0 + net6.0 - - - + - - + \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj b/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj index f7549b3e..c1fad611 100644 --- a/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj +++ b/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj @@ -1,15 +1,11 @@ - - net5.0 + net6.0 - - + - - - + \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj b/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj index 2f873ae2..da9a8a5b 100644 --- a/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj +++ b/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj @@ -1,17 +1,13 @@ - - net5.0 + net6.0 - - - - - + + + - - + \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj b/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj index 835f9d27..6de87bcb 100644 --- a/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj +++ b/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj b/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj index ab40863d..1860cd2c 100644 --- a/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj +++ b/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj @@ -1,19 +1,15 @@ - - net5.0 + net6.0 - - - - - + + + - - + \ No newline at end of file diff --git a/src/Equinox.Services.Api/Equinox.Services.Api.csproj b/src/Equinox.Services.Api/Equinox.Services.Api.csproj index b339cc6d..c632b61f 100644 --- a/src/Equinox.Services.Api/Equinox.Services.Api.csproj +++ b/src/Equinox.Services.Api/Equinox.Services.Api.csproj @@ -1,34 +1,29 @@ - - - net5.0 - b543be42-f7ab-48b6-b633-72d6fb529fb7 - Linux - ..\.. - - + + net6.0 + b543be42-f7ab-48b6-b633-72d6fb529fb7 + enable + Linux + ..\.. + - - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + - - - - - + + + + - - + \ No newline at end of file diff --git a/src/Equinox.Services.Api/Program.cs b/src/Equinox.Services.Api/Program.cs index 3c52f35a..d801f160 100644 --- a/src/Equinox.Services.Api/Program.cs +++ b/src/Equinox.Services.Api/Program.cs @@ -1,20 +1,69 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; +using Equinox.Infra.CrossCutting.Identity; +using Equinox.Services.Api.Configurations; +using MediatR; +using NetDevPack.Identity; +using NetDevPack.Identity.User; -namespace Equinox.Services.Api +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) + .AddEnvironmentVariables(); + +// ConfigureServices + +// WebAPI Config +builder.Services.AddControllers(); + +// Setting DBContexts +builder.Services.AddDatabaseConfiguration(builder.Configuration); + +// ASP.NET Identity Settings & JWT +builder.Services.AddApiIdentityConfiguration(builder.Configuration); + +// Interactive AspNetUser (logged in) +// NetDevPack.Identity dependency +builder.Services.AddAspNetUserConfiguration(); + +// AutoMapper Settings +builder.Services.AddAutoMapperConfiguration(); + +// Swagger Config +builder.Services.AddSwaggerConfiguration(); + +// Adding MediatR for Domain Events and Notifications +builder.Services.AddMediatR(AppDomain.CurrentDomain.GetAssemblies()); + +// .NET Native DI Abstraction +builder.Services.AddDependencyInjectionConfiguration(); + +var app = builder.Build(); + +// Configure + +if (app.Environment.IsDevelopment()) { - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } + app.UseDeveloperExceptionPage(); } + +app.UseHttpsRedirection(); + +app.UseRouting(); + +app.UseCors(c => +{ + c.AllowAnyHeader(); + c.AllowAnyMethod(); + c.AllowAnyOrigin(); +}); + +// NetDevPack.Identity dependency +app.UseAuthConfiguration(); + +app.MapControllers(); + +app.UseSwaggerSetup(); + +app.Run(); \ No newline at end of file diff --git a/src/Equinox.Services.Api/Startup.cs b/src/Equinox.Services.Api/Startup.cs deleted file mode 100644 index 3a205a4b..00000000 --- a/src/Equinox.Services.Api/Startup.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Equinox.Infra.CrossCutting.Identity; -using Equinox.Services.Api.Configurations; -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using NetDevPack.Identity; -using NetDevPack.Identity.User; - -namespace Equinox.Services.Api -{ - public class Startup - { - public Startup(IHostEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true); - - if (env.IsDevelopment()) - { - builder.AddUserSecrets(); - } - - builder.AddEnvironmentVariables(); - Configuration = builder.Build(); - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - // WebAPI Config - services.AddControllers(); - - // Setting DBContexts - services.AddDatabaseConfiguration(Configuration); - - // ASP.NET Identity Settings & JWT - services.AddApiIdentityConfiguration(Configuration); - - // Interactive AspNetUser (logged in) - // NetDevPack.Identity dependency - services.AddAspNetUserConfiguration(); - - // AutoMapper Settings - services.AddAutoMapperConfiguration(); - - // Swagger Config - services.AddSwaggerConfiguration(); - - // Adding MediatR for Domain Events and Notifications - services.AddMediatR(typeof(Startup)); - - // .NET Native DI Abstraction - services.AddDependencyInjectionConfiguration(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseCors(c => - { - c.AllowAnyHeader(); - c.AllowAnyMethod(); - c.AllowAnyOrigin(); - }); - - // NetDevPack.Identity dependency - app.UseAuthConfiguration(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - - app.UseSwaggerSetup(); - } - } -} diff --git a/src/Equinox.UI.Web/Equinox.UI.Web.csproj b/src/Equinox.UI.Web/Equinox.UI.Web.csproj index 4e29bcd0..f944e223 100644 --- a/src/Equinox.UI.Web/Equinox.UI.Web.csproj +++ b/src/Equinox.UI.Web/Equinox.UI.Web.csproj @@ -1,35 +1,31 @@ - - + - net5.0 + net6.0 aspnet-Equinox.UI.Web-61A38C1A-B3EE-4175-AD27-CD2A22786741 - false + enable Linux + ..\.. - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + - - - - + + + - + \ No newline at end of file diff --git a/src/Equinox.UI.Web/Program.cs b/src/Equinox.UI.Web/Program.cs index b22e3029..1657d32c 100644 --- a/src/Equinox.UI.Web/Program.cs +++ b/src/Equinox.UI.Web/Program.cs @@ -1,20 +1,76 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; +using Equinox.Infra.CrossCutting.Identity; +using Equinox.UI.Web.Configurations; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using NetDevPack.Identity; +using NetDevPack.Identity.User; -namespace Equinox.UI.Web +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) + .AddEnvironmentVariables(); + +// ConfigureServices + +builder.Services.AddControllersWithViews(options => +{ + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); +}); +builder.Services.AddRazorPages(); + +// Setting DBContexts +builder.Services.AddDatabaseConfiguration(builder.Configuration); + +// ASP.NET Identity Settings +builder.Services.AddWebAppIdentityConfiguration(builder.Configuration); + +// Authentication & Authorization +builder.Services.AddSocialAuthenticationConfiguration(builder.Configuration); + +// Interactive AspNetUser (logged in) +// NetDevPack.Identity dependency +builder.Services.AddAspNetUserConfiguration(); + +// AutoMapper Settings +builder.Services.AddAutoMapperConfiguration(); + +// Adding MediatR for Domain Events and Notifications +builder.Services.AddMediatR(AppDomain.CurrentDomain.GetAssemblies()); + +// .NET Native DI Abstraction +builder.Services.AddDependencyInjectionConfiguration(); + +var app = builder.Build(); + +// Configure + +if (app.Environment.IsDevelopment()) { - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); } +else +{ + app.UseExceptionHandler("/error/500"); + app.UseStatusCodePagesWithRedirects("/error/{0}"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +// NetDevPack.Identity dependency +app.UseAuthConfiguration(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + +app.MapRazorPages(); + +app.Run(); diff --git a/src/Equinox.UI.Web/Startup.cs b/src/Equinox.UI.Web/Startup.cs deleted file mode 100644 index 6b847f8b..00000000 --- a/src/Equinox.UI.Web/Startup.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Equinox.Infra.CrossCutting.Identity; -using Equinox.UI.Web.Configurations; -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using NetDevPack.Identity; -using NetDevPack.Identity.User; - -namespace Equinox.UI.Web -{ - public class Startup - { - public Startup(IHostEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true); - - if (env.IsDevelopment()) - { - builder.AddUserSecrets(); - } - - builder.AddEnvironmentVariables(); - Configuration = builder.Build(); - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - // MVC Settings - services.AddControllersWithViews(options => - { - options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); - }); - services.AddRazorPages(); - - // Setting DBContexts - services.AddDatabaseConfiguration(Configuration); - - // ASP.NET Identity Settings - services.AddWebAppIdentityConfiguration(Configuration); - - // Authentication & Authorization - services.AddSocialAuthenticationConfiguration(Configuration); - - // Interactive AspNetUser (logged in) - // NetDevPack.Identity dependency - services.AddAspNetUserConfiguration(); - - // AutoMapper Settings - services.AddAutoMapperConfiguration(); - - // Adding MediatR for Domain Events and Notifications - services.AddMediatR(typeof(Startup)); - - // .NET Native DI Abstraction - services.AddDependencyInjectionConfiguration(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseMigrationsEndPoint(); - } - else - { - app.UseExceptionHandler("/error/500"); - app.UseStatusCodePagesWithRedirects("/error/{0}"); - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - - // NetDevPack.Identity dependency - app.UseAuthConfiguration(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - endpoints.MapRazorPages(); - }); - } - } -} diff --git a/src/Equinox.UI.Web/Views/Home/Index.cshtml b/src/Equinox.UI.Web/Views/Home/Index.cshtml index 3da1c43a..c4e5a6c1 100644 --- a/src/Equinox.UI.Web/Views/Home/Index.cshtml +++ b/src/Equinox.UI.Web/Views/Home/Index.cshtml @@ -20,11 +20,11 @@

Technologies

    -
  • .NET 5.0
  • -
  • ASP.NET MVC Core 5.0
  • -
  • ASP.NET WebAPI Core 5.0
  • -
  • ASP.NET Identity Core 5.0
  • -
  • EF Core 5.0
  • +
  • .NET 6.0
  • +
  • ASP.NET MVC 6.0
  • +
  • ASP.NET WebAPI 6.0
  • +
  • ASP.NET Identity 6.0
  • +
  • EF Core 6.0
  • AutoMapper
  • FluentValidator
  • MediatR
  • diff --git a/src/Equinox.UI.Web/Views/Shared/_Layout.cshtml b/src/Equinox.UI.Web/Views/Shared/_Layout.cshtml index abc953d9..d5104656 100644 --- a/src/Equinox.UI.Web/Views/Shared/_Layout.cshtml +++ b/src/Equinox.UI.Web/Views/Shared/_Layout.cshtml @@ -34,7 +34,7 @@ diff --git a/src/Equinox.UI.Web/wwwroot/images/banner1.svg b/src/Equinox.UI.Web/wwwroot/images/banner1.svg index 3e347b6b..ebd3bfe5 100644 --- a/src/Equinox.UI.Web/wwwroot/images/banner1.svg +++ b/src/Equinox.UI.Web/wwwroot/images/banner1.svg @@ -13,7 +13,7 @@ Run your application anywhere - ASP.NET 5.0 + ASP.NET 6.0 From bf69e6c6ecfe1bf529c80e0a800cd2b7ee3f17dc Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Tue, 22 Mar 2022 05:02:56 -0300 Subject: [PATCH 03/21] Update Dockerfile --- src/Equinox.UI.Web/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Equinox.UI.Web/Dockerfile b/src/Equinox.UI.Web/Dockerfile index 70d1bc87..1c573b38 100644 --- a/src/Equinox.UI.Web/Dockerfile +++ b/src/Equinox.UI.Web/Dockerfile @@ -1,11 +1,11 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["Equinox.UI.Web/Equinox.UI.Web.csproj", "Equinox.UI.Web/"] RUN dotnet restore "Equinox.UI.Web/Equinox.UI.Web.csproj" @@ -19,4 +19,4 @@ RUN dotnet publish "Equinox.UI.Web.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Equinox.UI.Web.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "Equinox.UI.Web.dll"] From ca9b2ebc5e6604835422431d633180681deeb47e Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Tue, 22 Mar 2022 05:03:23 -0300 Subject: [PATCH 04/21] Update Dockerfile --- src/Equinox.Services.Api/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Equinox.Services.Api/Dockerfile b/src/Equinox.Services.Api/Dockerfile index bf17d5f1..479315de 100644 --- a/src/Equinox.Services.Api/Dockerfile +++ b/src/Equinox.Services.Api/Dockerfile @@ -1,11 +1,11 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["src/Equinox.Services.Api/Equinox.Services.Api.csproj", "src/Equinox.Services.Api/"] RUN dotnet restore "src/Equinox.Services.Api/Equinox.Services.Api.csproj" @@ -19,4 +19,4 @@ RUN dotnet publish "Equinox.Services.Api.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Equinox.Services.Api.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "Equinox.Services.Api.dll"] From 4c348d650960a30eb759f3d58ead5685ce622eb8 Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Tue, 22 Mar 2022 05:13:24 -0300 Subject: [PATCH 05/21] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 4a327b75..df553620 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,6 @@ To know more about how to setup your enviroment visit the [Microsoft .NET Downlo - Beware to use in production way - Maybe you don't need a lot of implementations that is included, try avoid the **over engineering** -## About the next versions -Watch our [RoadMap](https://github.com/EduardoPires/EquinoxProject/wiki/RoadMap) to know the new changes - ## Pull-Requests Make a contact! Don't submit PRs for extra features, all the new features are planned From dcbe05811ba0cd06dbbdbb02cf9db89b5544244b Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Wed, 31 Jul 2024 15:30:47 -0300 Subject: [PATCH 06/21] .net 8 migration and refactorings (#201) * .NET 8 * .NET 8 * Setup of Identity without external dependencies * Update README.md * Update dotnet-core.yml --- .github/workflows/dotnet-core.yml | 2 +- README.md | 12 +- .../Equinox.Application.csproj | 9 +- .../Equinox.Domain.Core.csproj | 3 +- src/Equinox.Domain/Equinox.Domain.csproj | 5 +- .../Equinox.Infra.CrossCutting.Bus.csproj | 5 +- .../API/AppJwtSettings.cs | 10 ++ .../API/JwtBuilder.cs | 117 +++++++++++++++++ .../ApiIdentityConfig.cs | 25 ---- .../CustomAuthorizationValidation.cs | 17 +++ .../Authorization/CustomAuthorizeAttribute.cs | 13 ++ .../Authorization/RequerimentClaimFilter.cs | 30 +++++ .../Configuration/AspNetIdentityConfig.cs | 124 ++++++++++++++++++ .../Data/EquinoxIdentityContext.cs | 10 ++ ...Equinox.Infra.CrossCutting.Identity.csproj | 22 +++- .../Extensions/ClaimsPrincipalExtensions.cs | 62 +++++++++ .../Models/LoginUser.cs | 15 +++ .../Models/RegisterUser.cs | 18 +++ .../Models/UserClaim.cs | 8 ++ .../Models/UserResponse.cs | 10 ++ .../Models/UserToken.cs | 11 ++ .../User/AspNetUser.cs | 50 +++++++ .../User/IAspNetUser.cs | 18 +++ .../WebAppIdentityConfig.cs | 21 --- .../Equinox.Infra.CrossCutting.IoC.csproj | 4 +- .../NativeInjectorBootStrapper.cs | 29 ++-- .../Equinox.Infra.Data.csproj | 14 +- .../EventSourcing/SqlEventStore.cs | 2 +- .../Configurations/ApiConfig.cs | 20 +++ .../Configurations/AutoMapperConfig.cs | 12 +- .../Configurations/DatabaseConfig.cs | 19 ++- .../DependencyInjectionConfig.cs | 12 +- .../Configurations/MediatRConfig.cs | 16 +++ .../Configurations/SwaggerConfig.cs | 49 +++++-- .../Controllers/AccountController.cs | 11 +- .../Controllers/ApiController.cs | 4 +- .../Controllers/CustomerController.cs | 17 +-- .../Equinox.Services.Api.csproj | 29 ++-- src/Equinox.Services.Api/Program.cs | 76 +++-------- .../Areas/Identity/IdentityHostingStartup.cs | 4 +- .../Areas/Identity/Pages/Account/Login.cshtml | 83 ++++++++++++ .../Identity/Pages/Account/Login.cshtml.cs | 110 ++++++++++++++++ .../Identity/Pages/Account/Logout.cshtml | 21 +++ .../Identity/Pages/Account/Logout.cshtml.cs | 43 ++++++ .../Identity/Pages/Account/Register.cshtml.cs | 16 +-- .../Configurations/AutoMapperConfig.cs | 12 +- .../Configurations/DatabaseConfig.cs | 19 ++- .../DependencyInjectionConfig.cs | 10 +- .../Configurations/IdentityConfig.cs | 23 +--- .../Configurations/MediatRConfig.cs | 16 +++ .../Configurations/MvcConfig.cs | 27 ++++ .../Controllers/BaseController.cs | 4 +- .../Controllers/CustomerController.cs | 4 +- .../00000000000000_CreateIdentitySchema.cs | 1 - src/Equinox.UI.Web/Equinox.UI.Web.csproj | 31 +++-- src/Equinox.UI.Web/Program.cs | 78 +++-------- .../Views/Customer/Index.cshtml | 2 +- src/Equinox.UI.Web/Views/Home/Index.cshtml | 10 +- src/Equinox.UI.Web/wwwroot/images/banner1.svg | 2 +- 59 files changed, 1093 insertions(+), 354 deletions(-) create mode 100644 src/Equinox.Infra.CrossCutting.Identity/API/AppJwtSettings.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/API/JwtBuilder.cs delete mode 100644 src/Equinox.Infra.CrossCutting.Identity/ApiIdentityConfig.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizationValidation.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizeAttribute.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Authorization/RequerimentClaimFilter.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Extensions/ClaimsPrincipalExtensions.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Models/LoginUser.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Models/RegisterUser.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Models/UserClaim.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Models/UserResponse.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Models/UserToken.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/User/AspNetUser.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/User/IAspNetUser.cs delete mode 100644 src/Equinox.Infra.CrossCutting.Identity/WebAppIdentityConfig.cs create mode 100644 src/Equinox.Services.Api/Configurations/ApiConfig.cs create mode 100644 src/Equinox.Services.Api/Configurations/MediatRConfig.cs create mode 100644 src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml create mode 100644 src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml.cs create mode 100644 src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml create mode 100644 src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs create mode 100644 src/Equinox.UI.Web/Configurations/MediatRConfig.cs create mode 100644 src/Equinox.UI.Web/Configurations/MvcConfig.cs diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index e8608446..a341f7fc 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/README.md b/README.md index df553620..ca6d4350 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ The Equinox Project is a open-source project written in .NET Core The goal of this project is implement the most common used technologies and share with the technical community the best way to develop great applications with .NET [![Build status](https://ci.appveyor.com/api/projects/status/rl2ja69994rt3ei6?svg=true)](https://ci.appveyor.com/project/EduardoPires/equinoxproject) -![.NET Core](https://github.com/EduardoPires/EquinoxProject/workflows/.NET%20Core/badge.svg) [![License](https://img.shields.io/github/license/eduardopires/equinoxproject.svg)](LICENSE) [![Issues open](https://img.shields.io/github/issues/eduardopires/equinoxproject.svg)](https://huboard.com/EduardoPires/EquinoxProject/) @@ -29,18 +28,17 @@ To know more about how to setup your enviroment visit the [Microsoft .NET Downlo ## Technologies implemented: -- ASP.NET 6.0 +- ASP.NET 8.0 - ASP.NET MVC Core - ASP.NET WebApi Core with JWT Bearer Authentication - ASP.NET Identity Core -- Entity Framework Core 6.0 +- Entity Framework Core 8.0 - .NET Core Native DI - AutoMapper - FluentValidator - MediatR - Swagger UI with JWT support - .NET DevPack -- .NET DevPack.Identity ## Architecture: @@ -56,6 +54,12 @@ To know more about how to setup your enviroment visit the [Microsoft .NET Downlo ## News +**v1.9 - 06/31/2024** +- Migrated for .NET 8.0 +- Full refactoring of Web and Api configuration +- Now all ASP.NET Identity configurations are inside the project, without external dependencies +- All dependencies is up to date + **v1.8 - 03/22/2022** - Migrated for .NET 6.0 - All dependencies is up to date diff --git a/src/Equinox.Application/Equinox.Application.csproj b/src/Equinox.Application/Equinox.Application.csproj index 3a5b59ed..b3df6541 100644 --- a/src/Equinox.Application/Equinox.Application.csproj +++ b/src/Equinox.Application/Equinox.Application.csproj @@ -1,12 +1,13 @@ - net6.0 + net8.0 + disable - - + + - + \ No newline at end of file diff --git a/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj b/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj index 8e37f651..f4bfdf36 100644 --- a/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj +++ b/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj @@ -1,6 +1,7 @@ - net6.0 + net8.0 + disable diff --git a/src/Equinox.Domain/Equinox.Domain.csproj b/src/Equinox.Domain/Equinox.Domain.csproj index 472d8dfb..f7a018b6 100644 --- a/src/Equinox.Domain/Equinox.Domain.csproj +++ b/src/Equinox.Domain/Equinox.Domain.csproj @@ -1,13 +1,14 @@ - net6.0 + net8.0 + disable - + diff --git a/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj b/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj index c1fad611..01715596 100644 --- a/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj +++ b/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj @@ -1,9 +1,10 @@ - net6.0 + net8.0 + disable - + diff --git a/src/Equinox.Infra.CrossCutting.Identity/API/AppJwtSettings.cs b/src/Equinox.Infra.CrossCutting.Identity/API/AppJwtSettings.cs new file mode 100644 index 00000000..26fb6627 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/API/AppJwtSettings.cs @@ -0,0 +1,10 @@ +namespace Equinox.Infra.CrossCutting.Identity.API +{ + public class AppJwtSettings + { + public string SecretKey { get; set; } + public int Expiration { get; set; } = 1; + public string Issuer { get; set; } = "Equinox.Api"; + public string Audience { get; set; } = "Api"; + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/API/JwtBuilder.cs b/src/Equinox.Infra.CrossCutting.Identity/API/JwtBuilder.cs new file mode 100644 index 00000000..89840296 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/API/JwtBuilder.cs @@ -0,0 +1,117 @@ +using Equinox.Infra.CrossCutting.Identity.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; + +namespace Equinox.Infra.CrossCutting.Identity.API +{ + public class JwtBuilder where TIdentityUser : IdentityUser where TKey : IEquatable + { + private UserManager _userManager; + private AppJwtSettings _appJwtSettings; + private TIdentityUser _user; + private ICollection _userClaims; + private ICollection _jwtClaims; + private ClaimsIdentity _identityClaims; + + public JwtBuilder WithUserManager(UserManager userManager) + { + _userManager = userManager ?? throw new ArgumentException(nameof(userManager)); + return this; + } + + public JwtBuilder WithJwtSettings(AppJwtSettings appJwtSettings) + { + _appJwtSettings = appJwtSettings ?? throw new ArgumentException(nameof(appJwtSettings)); + return this; + } + + public JwtBuilder WithEmail(string email) + { + if (string.IsNullOrEmpty(email)) throw new ArgumentException(nameof(email)); + + _user = _userManager.FindByEmailAsync(email).Result; + _userClaims = new List(); + _jwtClaims = new List(); + _identityClaims = new ClaimsIdentity(); + + return this; + } + + public JwtBuilder WithJwtClaims() + { + _jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Sub, _user.Id.ToString())); + _jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Email, _user.Email)); + _jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())); + _jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Nbf, ToUnixEpochDate(DateTime.UtcNow).ToString())); + _jwtClaims.Add(new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(DateTime.UtcNow).ToString(), ClaimValueTypes.Integer64)); + + _identityClaims.AddClaims(_jwtClaims); + + return this; + } + + public JwtBuilder WithUserClaims() + { + _userClaims = _userManager.GetClaimsAsync(_user).Result; + _identityClaims.AddClaims(_userClaims); + + return this; + } + + public JwtBuilder WithUserRoles() + { + var userRoles = _userManager.GetRolesAsync(_user).Result; + userRoles.ToList().ForEach(r => _identityClaims.AddClaim(new Claim("role", r))); + + return this; + } + + public string BuildToken() + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_appJwtSettings.SecretKey); + var token = tokenHandler.CreateToken(new SecurityTokenDescriptor + { + Issuer = _appJwtSettings.Issuer, + Audience = _appJwtSettings.Audience, + Subject = _identityClaims, + Expires = DateTime.UtcNow.AddHours(_appJwtSettings.Expiration), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256Signature) + }); + + return tokenHandler.WriteToken(token); + } + + public UserResponse BuildUserResponse() + { + var user = new UserResponse + { + AccessToken = BuildToken(), + ExpiresIn = TimeSpan.FromHours(_appJwtSettings.Expiration).TotalSeconds, + UserToken = new UserToken + { + Id = _user.Id, + Email = _user.Email, + Claims = _userClaims.Select(c => new UserClaim { Type = c.Type, Value = c.Value }) + } + }; + + return user; + } + + private static long ToUnixEpochDate(DateTime date) + => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) + .TotalSeconds); + } + + public class JwtBuilder : JwtBuilder where TIdentityUser : IdentityUser { } + + public sealed class JwtBuilder : JwtBuilder { } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/ApiIdentityConfig.cs b/src/Equinox.Infra.CrossCutting.Identity/ApiIdentityConfig.cs deleted file mode 100644 index 99fda35b..00000000 --- a/src/Equinox.Infra.CrossCutting.Identity/ApiIdentityConfig.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NetDevPack.Identity; -using NetDevPack.Identity.Jwt; - -namespace Equinox.Infra.CrossCutting.Identity -{ - public static class ApiIdentityConfig - { - public static void AddApiIdentityConfiguration(this IServiceCollection services, IConfiguration configuration) - { - // Default EF Context for Identity (inside of the NetDevPack.Identity) - services.AddIdentityEntityFrameworkContextConfiguration(options => - options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"), - b => b.MigrationsAssembly("Equinox.Infra.CrossCutting.Identity"))); - - // Default Identity configuration from NetDevPack.Identity - services.AddIdentityConfiguration(); - - // Default JWT configuration from NetDevPack.Identity - services.AddJwtConfiguration(configuration, "AppSettings"); - } - } -} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizationValidation.cs b/src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizationValidation.cs new file mode 100644 index 00000000..3c7be705 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizationValidation.cs @@ -0,0 +1,17 @@ +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace Equinox.Infra.CrossCutting.Identity.Authorization +{ + public static class CustomAuthorizationValidation + { + public static bool UserHasValidClaim(HttpContext context, string claimName, string claimValue) + { + return context.User.Identity.IsAuthenticated && + context.User.Claims.Any(c => + c.Type == claimName && + c.Value.Split(',').Select(v => v.Trim()).Contains(claimValue)); + } + + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizeAttribute.cs b/src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizeAttribute.cs new file mode 100644 index 00000000..7e946872 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizeAttribute.cs @@ -0,0 +1,13 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; + +namespace Equinox.Infra.CrossCutting.Identity.Authorization +{ + public class CustomAuthorizeAttribute : TypeFilterAttribute + { + public CustomAuthorizeAttribute(string claimName, string claimValue) : base(typeof(RequerimentClaimFilter)) + { + Arguments = new object[] { new Claim(claimName, claimValue) }; + } + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Authorization/RequerimentClaimFilter.cs b/src/Equinox.Infra.CrossCutting.Identity/Authorization/RequerimentClaimFilter.cs new file mode 100644 index 00000000..680606cf --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Authorization/RequerimentClaimFilter.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Equinox.Infra.CrossCutting.Identity.Authorization +{ + internal class RequerimentClaimFilter : IAuthorizationFilter + { + private readonly Claim _claim; + + public RequerimentClaimFilter(Claim claim) + { + _claim = claim; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext.User.Identity.IsAuthenticated) + { + context.Result = new StatusCodeResult(401); + return; + } + + if (!CustomAuthorizationValidation.UserHasValidClaim(context.HttpContext, _claim.Type, _claim.Value)) + { + context.Result = new StatusCodeResult(403); + } + } + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs b/src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs new file mode 100644 index 00000000..16b342ad --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs @@ -0,0 +1,124 @@ +using Equinox.Infra.CrossCutting.Identity.API; +using Equinox.Infra.CrossCutting.Identity.Data; +using Equinox.Infra.CrossCutting.Identity.User; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Text; + +namespace Equinox.Infra.CrossCutting.Identity.Configuration +{ + public static class AspNetIdentityConfig + { + public static WebApplicationBuilder AddApiIdentityConfiguration(this WebApplicationBuilder builder) + { + builder.AddIdentityDbContext() + .AddIdentityApiSupport() + .AddJwtSupport() + .AddAspNetUserSupport(); + + return builder; + } + + public static WebApplicationBuilder AddWebIdentityConfiguration(this WebApplicationBuilder builder) + { + builder.AddIdentityDbContext() + .AddIdentityWebUISupport() + .AddAspNetUserSupport() + .AddSocialAuthenticationSupport(); + + return builder; + } + + private static WebApplicationBuilder AddIdentityDbContext(this WebApplicationBuilder builder) + { + builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), + b => b.MigrationsAssembly("Equinox.Infra.CrossCutting.Identity.Data"))); + + return builder; + } + + private static WebApplicationBuilder AddIdentityApiSupport(this WebApplicationBuilder builder) + { + builder.Services.AddIdentityApiEndpoints() + .AddRoles() + .AddEntityFrameworkStores() + .AddSignInManager() + .AddRoleManager>() + .AddDefaultTokenProviders(); + + return builder; + } + + private static WebApplicationBuilder AddIdentityWebUISupport(this WebApplicationBuilder builder) + { + builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders() + .AddDefaultUI(); + + return builder; + } + + private static WebApplicationBuilder AddJwtSupport(this WebApplicationBuilder builder) + { + var appSettingsSection = builder.Configuration.GetSection("AppSettings"); + builder.Services.Configure(appSettingsSection); + + var appSettings = appSettingsSection.Get(); + var key = Encoding.ASCII.GetBytes(appSettings.SecretKey); + + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = true; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = appSettings.Audience, + ValidIssuer = appSettings.Issuer + }; + }); + + return builder; + } + + public static WebApplicationBuilder AddAspNetUserSupport(this WebApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + + return builder; + } + + public static WebApplicationBuilder AddSocialAuthenticationSupport(this WebApplicationBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.Services.AddAuthentication() + .AddFacebook(o => + { + o.AppId = builder.Configuration["Authentication:Facebook:AppId"]; + o.AppSecret = builder.Configuration["Authentication:Facebook:AppSecret"]; + }) + .AddGoogle(googleOptions => + { + googleOptions.ClientId = builder.Configuration["Authentication:Google:ClientId"]; + googleOptions.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; + }); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs b/src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs new file mode 100644 index 00000000..eacd8e71 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Equinox.Infra.CrossCutting.Identity.Data +{ + public class EquinoxIdentityContext : IdentityDbContext + { + public EquinoxIdentityContext(DbContextOptions options) : base(options) { } + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj b/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj index da9a8a5b..fce7630a 100644 --- a/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj +++ b/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj @@ -1,13 +1,25 @@ - + - net6.0 + net8.0 + disable - - - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Extensions/ClaimsPrincipalExtensions.cs b/src/Equinox.Infra.CrossCutting.Identity/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 00000000..3f3182b6 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,62 @@ +using System.Security.Claims; +using System; +using System.IdentityModel.Tokens.Jwt; + +namespace Equinox.Infra.CrossCutting.Identity.Extensions +{ + public static class ClaimsPrincipalExtensions + { + public static string GetUserId(this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + + var claim = principal.FindFirst(JwtRegisteredClaimNames.Sub); + if (claim is null) + claim = principal.FindFirst(ClaimTypes.NameIdentifier); + + return claim?.Value; + } + + public static string GetUserEmail(this ClaimsPrincipal principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + var claim = principal.FindFirst(JwtRegisteredClaimNames.Sub); + if (claim is null) + claim = principal.FindFirst(ClaimTypes.Email); + + return claim?.Value; + } + public static string GetUserId(this ClaimsIdentity principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + + var claim = principal.FindFirst(JwtRegisteredClaimNames.Sub); + if (claim is null) + claim = principal.FindFirst(ClaimTypes.NameIdentifier); + + return claim?.Value; + } + + public static string GetUserEmail(this ClaimsIdentity principal) + { + if (principal == null) + { + throw new ArgumentException(nameof(principal)); + } + var claim = principal.FindFirst(JwtRegisteredClaimNames.Sub); + if (claim is null) + claim = principal.FindFirst(ClaimTypes.Email); + + return claim?.Value; + } + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Models/LoginUser.cs b/src/Equinox.Infra.CrossCutting.Identity/Models/LoginUser.cs new file mode 100644 index 00000000..349d64e9 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Models/LoginUser.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Equinox.Infra.CrossCutting.Identity.Models +{ + public class LoginUser + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, MinimumLength = 6)] + public string Password { get; set; } + } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/Models/RegisterUser.cs b/src/Equinox.Infra.CrossCutting.Identity/Models/RegisterUser.cs new file mode 100644 index 00000000..bb0a12e0 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Models/RegisterUser.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Equinox.Infra.CrossCutting.Identity.Models +{ + public class RegisterUser + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, MinimumLength = 6)] + public string Password { get; set; } + + [Compare("Password")] + public string ConfirmPassword { get; set; } + } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/Models/UserClaim.cs b/src/Equinox.Infra.CrossCutting.Identity/Models/UserClaim.cs new file mode 100644 index 00000000..c37ab8e6 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Models/UserClaim.cs @@ -0,0 +1,8 @@ +namespace Equinox.Infra.CrossCutting.Identity.Models +{ + public class UserClaim + { + public string Value { get; set; } + public string Type { get; set; } + } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/Models/UserResponse.cs b/src/Equinox.Infra.CrossCutting.Identity/Models/UserResponse.cs new file mode 100644 index 00000000..60c03a4d --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Models/UserResponse.cs @@ -0,0 +1,10 @@ +namespace Equinox.Infra.CrossCutting.Identity.Models +{ + public class UserResponse + { + public string AccessToken { get; set; } + public double ExpiresIn { get; set; } + public UserToken UserToken { get; set; } + public string RefreshToken { get; set; } + } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/Models/UserToken.cs b/src/Equinox.Infra.CrossCutting.Identity/Models/UserToken.cs new file mode 100644 index 00000000..2897becc --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Models/UserToken.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Equinox.Infra.CrossCutting.Identity.Models +{ + public class UserToken + { + public dynamic Id { get; set; } + public string Email { get; set; } + public IEnumerable Claims { get; set; } + } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/User/AspNetUser.cs b/src/Equinox.Infra.CrossCutting.Identity/User/AspNetUser.cs new file mode 100644 index 00000000..b70d5c7f --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/User/AspNetUser.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Equinox.Infra.CrossCutting.Identity.Extensions; +using Microsoft.AspNetCore.Http; + +namespace Equinox.Infra.CrossCutting.Identity.User +{ + public class AspNetUser : IAspNetUser + { + private readonly IHttpContextAccessor _accessor; + + public AspNetUser(IHttpContextAccessor accessor) + { + _accessor = accessor; + } + + public string Name => _accessor.HttpContext.User.Identity.Name; + + public Guid GetUserId() + { + return IsAutenticated() ? Guid.Parse(_accessor.HttpContext.User.GetUserId()) : Guid.Empty; + } + + public string GetUserEmail() + { + return IsAutenticated() ? _accessor.HttpContext.User.GetUserEmail() : ""; + } + + public bool IsAutenticated() + { + return _accessor.HttpContext.User.Identity.IsAuthenticated; + } + + public bool IsInRole(string role) + { + return _accessor.HttpContext.User.IsInRole(role); + } + + public IEnumerable GetUserClaims() + { + return _accessor.HttpContext.User.Claims; + } + + public HttpContext GetHttpContext() + { + return _accessor.HttpContext; + } + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/User/IAspNetUser.cs b/src/Equinox.Infra.CrossCutting.Identity/User/IAspNetUser.cs new file mode 100644 index 00000000..59a1087a --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/User/IAspNetUser.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace Equinox.Infra.CrossCutting.Identity.User +{ + public interface IAspNetUser + { + string Name { get; } + Guid GetUserId(); + string GetUserEmail(); + bool IsAutenticated(); + bool IsInRole(string role); + IEnumerable GetUserClaims(); + HttpContext GetHttpContext(); + } +} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/WebAppIdentityConfig.cs b/src/Equinox.Infra.CrossCutting.Identity/WebAppIdentityConfig.cs deleted file mode 100644 index e2ca0b7b..00000000 --- a/src/Equinox.Infra.CrossCutting.Identity/WebAppIdentityConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NetDevPack.Identity; - -namespace Equinox.Infra.CrossCutting.Identity -{ - public static class WebAppIdentityConfig - { - public static void AddWebAppIdentityConfiguration(this IServiceCollection services, IConfiguration configuration) - { - // Default EF Context for Identity (inside of the NetDevPack.Identity) - services.AddIdentityEntityFrameworkContextConfiguration(options => - SqlServerDbContextOptionsExtensions.UseSqlServer(options, configuration.GetConnectionString("DefaultConnection"), - b => b.MigrationsAssembly("Equinox.Infra.CrossCutting.Identity"))); - - // Default Identity configuration from NetDevPack.Identity - services.AddIdentityConfiguration(); - } - } -} \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj b/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj index 6de87bcb..e7606282 100644 --- a/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj +++ b/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj @@ -1,13 +1,13 @@ - net6.0 + net8.0 + disable - diff --git a/src/Equinox.Infra.CrossCutting.IoC/NativeInjectorBootStrapper.cs b/src/Equinox.Infra.CrossCutting.IoC/NativeInjectorBootStrapper.cs index 885f9a7d..2f5dd1a5 100644 --- a/src/Equinox.Infra.CrossCutting.IoC/NativeInjectorBootStrapper.cs +++ b/src/Equinox.Infra.CrossCutting.IoC/NativeInjectorBootStrapper.cs @@ -11,6 +11,7 @@ using Equinox.Infra.Data.Repository.EventSourcing; using FluentValidation.Results; using MediatR; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using NetDevPack.Mediator; @@ -18,32 +19,32 @@ namespace Equinox.Infra.CrossCutting.IoC { public static class NativeInjectorBootStrapper { - public static void RegisterServices(IServiceCollection services) + public static void RegisterServices(WebApplicationBuilder builder) { // Domain Bus (Mediator) - services.AddScoped(); + builder.Services.AddScoped(); // Application - services.AddScoped(); + builder.Services.AddScoped(); // Domain - Events - services.AddScoped, CustomerEventHandler>(); - services.AddScoped, CustomerEventHandler>(); - services.AddScoped, CustomerEventHandler>(); + builder.Services.AddScoped, CustomerEventHandler>(); + builder.Services.AddScoped, CustomerEventHandler>(); + builder.Services.AddScoped, CustomerEventHandler>(); // Domain - Commands - services.AddScoped, CustomerCommandHandler>(); - services.AddScoped, CustomerCommandHandler>(); - services.AddScoped, CustomerCommandHandler>(); + builder.Services.AddScoped, CustomerCommandHandler>(); + builder.Services.AddScoped, CustomerCommandHandler>(); + builder.Services.AddScoped, CustomerCommandHandler>(); // Infra - Data - services.AddScoped(); - services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // Infra - Data EventSourcing - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); } } } \ No newline at end of file diff --git a/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj b/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj index 1860cd2c..cd1b3a85 100644 --- a/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj +++ b/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj @@ -1,15 +1,19 @@ - net6.0 + net8.0 + disable - - - - + + + + + + + \ No newline at end of file diff --git a/src/Equinox.Infra.Data/EventSourcing/SqlEventStore.cs b/src/Equinox.Infra.Data/EventSourcing/SqlEventStore.cs index dac91b12..90c2deeb 100644 --- a/src/Equinox.Infra.Data/EventSourcing/SqlEventStore.cs +++ b/src/Equinox.Infra.Data/EventSourcing/SqlEventStore.cs @@ -1,6 +1,6 @@ using Equinox.Domain.Core.Events; +using Equinox.Infra.CrossCutting.Identity.User; using Equinox.Infra.Data.Repository.EventSourcing; -using NetDevPack.Identity.User; using NetDevPack.Messaging; using Newtonsoft.Json; diff --git a/src/Equinox.Services.Api/Configurations/ApiConfig.cs b/src/Equinox.Services.Api/Configurations/ApiConfig.cs new file mode 100644 index 00000000..80fba996 --- /dev/null +++ b/src/Equinox.Services.Api/Configurations/ApiConfig.cs @@ -0,0 +1,20 @@ +namespace Equinox.Services.Api.Configurations +{ + public static class ApiConfig + { + public static WebApplicationBuilder AddApiConfiguration(this WebApplicationBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.Configuration + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) + .AddEnvironmentVariables(); + + builder.Services.AddControllers(); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Equinox.Services.Api/Configurations/AutoMapperConfig.cs b/src/Equinox.Services.Api/Configurations/AutoMapperConfig.cs index 1d7efcba..15126f77 100644 --- a/src/Equinox.Services.Api/Configurations/AutoMapperConfig.cs +++ b/src/Equinox.Services.Api/Configurations/AutoMapperConfig.cs @@ -1,16 +1,16 @@ -using System; -using Equinox.Application.AutoMapper; -using Microsoft.Extensions.DependencyInjection; +using Equinox.Application.AutoMapper; namespace Equinox.Services.Api.Configurations { public static class AutoMapperConfig { - public static void AddAutoMapperConfiguration(this IServiceCollection services) + public static WebApplicationBuilder AddAutoMapperConfiguration(this WebApplicationBuilder builder) { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - services.AddAutoMapper(typeof(DomainToViewModelMappingProfile), typeof(ViewModelToDomainMappingProfile)); + builder.Services.AddAutoMapper(typeof(DomainToViewModelMappingProfile), typeof(ViewModelToDomainMappingProfile)); + + return builder; } } } \ No newline at end of file diff --git a/src/Equinox.Services.Api/Configurations/DatabaseConfig.cs b/src/Equinox.Services.Api/Configurations/DatabaseConfig.cs index adbafe60..a6eb3071 100644 --- a/src/Equinox.Services.Api/Configurations/DatabaseConfig.cs +++ b/src/Equinox.Services.Api/Configurations/DatabaseConfig.cs @@ -1,22 +1,21 @@ -using System; -using Equinox.Infra.Data.Context; +using Equinox.Infra.Data.Context; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace Equinox.Services.Api.Configurations { public static class DatabaseConfig { - public static void AddDatabaseConfiguration(this IServiceCollection services, IConfiguration configuration) + public static WebApplicationBuilder AddDatabaseConfiguration(this WebApplicationBuilder builder) { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - services.AddDbContext(options => - options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); + builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); - services.AddDbContext(options => - options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); + builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + + return builder; } } } \ No newline at end of file diff --git a/src/Equinox.Services.Api/Configurations/DependencyInjectionConfig.cs b/src/Equinox.Services.Api/Configurations/DependencyInjectionConfig.cs index 9b54a449..39f0d026 100644 --- a/src/Equinox.Services.Api/Configurations/DependencyInjectionConfig.cs +++ b/src/Equinox.Services.Api/Configurations/DependencyInjectionConfig.cs @@ -1,16 +1,16 @@ -using System; -using Equinox.Infra.CrossCutting.IoC; -using Microsoft.Extensions.DependencyInjection; +using Equinox.Infra.CrossCutting.IoC; namespace Equinox.Services.Api.Configurations { public static class DependencyInjectionConfig { - public static void AddDependencyInjectionConfiguration(this IServiceCollection services) + public static WebApplicationBuilder AddDependencyInjectionConfiguration(this WebApplicationBuilder builder) { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - NativeInjectorBootStrapper.RegisterServices(services); + NativeInjectorBootStrapper.RegisterServices(builder); + + return builder; } } } \ No newline at end of file diff --git a/src/Equinox.Services.Api/Configurations/MediatRConfig.cs b/src/Equinox.Services.Api/Configurations/MediatRConfig.cs new file mode 100644 index 00000000..aff75658 --- /dev/null +++ b/src/Equinox.Services.Api/Configurations/MediatRConfig.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +namespace Equinox.Services.Api.Configurations +{ + public static class MediatRConfig + { + public static WebApplicationBuilder AddMediatRConfiguration(this WebApplicationBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Equinox.Services.Api/Configurations/SwaggerConfig.cs b/src/Equinox.Services.Api/Configurations/SwaggerConfig.cs index 60fd1cdd..d52185cc 100644 --- a/src/Equinox.Services.Api/Configurations/SwaggerConfig.cs +++ b/src/Equinox.Services.Api/Configurations/SwaggerConfig.cs @@ -1,17 +1,16 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Models; namespace Equinox.Services.Api.Configurations { public static class SwaggerConfig { - public static void AddSwaggerConfiguration(this IServiceCollection services) + public static WebApplicationBuilder AddSwaggerConfiguration(this WebApplicationBuilder builder) { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - services.AddSwaggerGen(s => + builder.Services.AddEndpointsApiExplorer(); + + builder.Services.AddSwaggerGen(s => { s.SwaggerDoc("v1", new OpenApiInfo { @@ -47,10 +46,42 @@ public static void AddSwaggerConfiguration(this IServiceCollection services) } }); + // Excluding ASP.NET Identity endpoints + s.DocInclusionPredicate((docName, apiDesc) => + { + var relativePath = apiDesc.RelativePath; + + // List of avoid patches + var identityEndpoints = new[] + { + "register", + "manage", + "refresh", + "login", + "confirmEmail", + "resendConfirmationEmail", + "forgotPassword", + "resetPassword" + }; + + // Validating if the endpoint is avoided + foreach (var endpoint in identityEndpoints) + { + if (relativePath.Contains(endpoint, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + }); + }); + + return builder; } - public static void UseSwaggerSetup(this IApplicationBuilder app) + public static IApplicationBuilder UseSwaggerSetup(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); @@ -59,6 +90,8 @@ public static void UseSwaggerSetup(this IApplicationBuilder app) { c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); }); + + return app; } } } \ No newline at end of file diff --git a/src/Equinox.Services.Api/Controllers/AccountController.cs b/src/Equinox.Services.Api/Controllers/AccountController.cs index 07416c97..6617d497 100644 --- a/src/Equinox.Services.Api/Controllers/AccountController.cs +++ b/src/Equinox.Services.Api/Controllers/AccountController.cs @@ -1,13 +1,12 @@ -using System.Threading.Tasks; +using Equinox.Infra.CrossCutting.Identity.API; +using Equinox.Infra.CrossCutting.Identity.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using NetDevPack.Identity.Jwt; -using NetDevPack.Identity.Model; namespace Equinox.Services.Api.Controllers { - [Route("api/[controller]")] + [Route("account")] [ApiController] public class AccountController : ApiController { @@ -26,7 +25,7 @@ public AccountController( } [HttpPost] - [Route("register")] + [Route("new")] public async Task Register(RegisterUser registerUser) { if (!ModelState.IsValid) return CustomResponse(ModelState); @@ -54,7 +53,7 @@ public async Task Register(RegisterUser registerUser) } [HttpPost] - [Route("login")] + [Route("enter")] public async Task Login(LoginUser loginUser) { if (!ModelState.IsValid) return CustomResponse(ModelState); diff --git a/src/Equinox.Services.Api/Controllers/ApiController.cs b/src/Equinox.Services.Api/Controllers/ApiController.cs index 6f518ae2..8266fbf9 100644 --- a/src/Equinox.Services.Api/Controllers/ApiController.cs +++ b/src/Equinox.Services.Api/Controllers/ApiController.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation.Results; +using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; diff --git a/src/Equinox.Services.Api/Controllers/CustomerController.cs b/src/Equinox.Services.Api/Controllers/CustomerController.cs index cc32d374..e900dc17 100644 --- a/src/Equinox.Services.Api/Controllers/CustomerController.cs +++ b/src/Equinox.Services.Api/Controllers/CustomerController.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Equinox.Application.EventSourcedNormalizers; using Equinox.Application.Interfaces; using Equinox.Application.ViewModels; +using Equinox.Infra.CrossCutting.Identity.Authorization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using NetDevPack.Identity.Authorization; namespace Equinox.Services.Api.Controllers { @@ -21,42 +18,42 @@ public CustomerController(ICustomerAppService customerAppService) } [AllowAnonymous] - [HttpGet("customer-management")] + [HttpGet("customer")] public async Task> Get() { return await _customerAppService.GetAll(); } [AllowAnonymous] - [HttpGet("customer-management/{id:guid}")] + [HttpGet("customer/{id:guid}")] public async Task Get(Guid id) { return await _customerAppService.GetById(id); } [CustomAuthorize("Customers", "Write")] - [HttpPost("customer-management")] + [HttpPost("customer")] public async Task Post([FromBody]CustomerViewModel customerViewModel) { return !ModelState.IsValid ? CustomResponse(ModelState) : CustomResponse(await _customerAppService.Register(customerViewModel)); } [CustomAuthorize("Customers", "Write")] - [HttpPut("customer-management")] + [HttpPut("customer")] public async Task Put([FromBody]CustomerViewModel customerViewModel) { return !ModelState.IsValid ? CustomResponse(ModelState) : CustomResponse(await _customerAppService.Update(customerViewModel)); } [CustomAuthorize("Customers", "Remove")] - [HttpDelete("customer-management")] + [HttpDelete("customer")] public async Task Delete(Guid id) { return CustomResponse(await _customerAppService.Remove(id)); } [AllowAnonymous] - [HttpGet("customer-management/history/{id:guid}")] + [HttpGet("customer/history/{id:guid}")] public async Task> History(Guid id) { return await _customerAppService.GetAllHistory(id); diff --git a/src/Equinox.Services.Api/Equinox.Services.Api.csproj b/src/Equinox.Services.Api/Equinox.Services.Api.csproj index c632b61f..4296d3e5 100644 --- a/src/Equinox.Services.Api/Equinox.Services.Api.csproj +++ b/src/Equinox.Services.Api/Equinox.Services.Api.csproj @@ -1,29 +1,30 @@ - net6.0 + net8.0 + disable b543be42-f7ab-48b6-b633-72d6fb529fb7 enable Linux ..\.. - - - - - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + \ No newline at end of file diff --git a/src/Equinox.Services.Api/Program.cs b/src/Equinox.Services.Api/Program.cs index d801f160..146bb824 100644 --- a/src/Equinox.Services.Api/Program.cs +++ b/src/Equinox.Services.Api/Program.cs @@ -1,69 +1,33 @@ -using Equinox.Infra.CrossCutting.Identity; +using Equinox.Infra.CrossCutting.Identity.Configuration; using Equinox.Services.Api.Configurations; -using MediatR; -using NetDevPack.Identity; -using NetDevPack.Identity.User; +using Microsoft.AspNetCore.Identity; var builder = WebApplication.CreateBuilder(args); -builder.Configuration - .SetBasePath(builder.Environment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) - .AddEnvironmentVariables(); - -// ConfigureServices - -// WebAPI Config -builder.Services.AddControllers(); - -// Setting DBContexts -builder.Services.AddDatabaseConfiguration(builder.Configuration); - -// ASP.NET Identity Settings & JWT -builder.Services.AddApiIdentityConfiguration(builder.Configuration); - -// Interactive AspNetUser (logged in) -// NetDevPack.Identity dependency -builder.Services.AddAspNetUserConfiguration(); - -// AutoMapper Settings -builder.Services.AddAutoMapperConfiguration(); - -// Swagger Config -builder.Services.AddSwaggerConfiguration(); - -// Adding MediatR for Domain Events and Notifications -builder.Services.AddMediatR(AppDomain.CurrentDomain.GetAssemblies()); - -// .NET Native DI Abstraction -builder.Services.AddDependencyInjectionConfiguration(); +// Configure Services +builder.AddApiConfiguration() // Api Configurations + .AddDatabaseConfiguration() // Setting DBContexts + .AddApiIdentityConfiguration() // ASP.NET Identity Settings & JWT + .AddAutoMapperConfiguration() // AutoMapper Settings + .AddSwaggerConfiguration() // Swagger Config + .AddMediatRConfiguration() // Adding MediatR for Domain Events and Notifications + .AddDependencyInjectionConfiguration(); // DotNet Native DI Abstraction var app = builder.Build(); // Configure - -if (app.Environment.IsDevelopment()) -{ - app.UseDeveloperExceptionPage(); -} - -app.UseHttpsRedirection(); - -app.UseRouting(); - -app.UseCors(c => -{ - c.AllowAnyHeader(); - c.AllowAnyMethod(); - c.AllowAnyOrigin(); -}); - -// NetDevPack.Identity dependency -app.UseAuthConfiguration(); +app.UseHttpsRedirection() + .UseCors(c => + { + c.AllowAnyHeader(); + c.AllowAnyMethod(); + c.AllowAnyOrigin(); + }) + .UseAuthentication() + .UseAuthorization(); app.MapControllers(); +app.MapIdentityApi(); app.UseSwaggerSetup(); - app.Run(); \ No newline at end of file diff --git a/src/Equinox.UI.Web/Areas/Identity/IdentityHostingStartup.cs b/src/Equinox.UI.Web/Areas/Identity/IdentityHostingStartup.cs index d0cba12d..94a44287 100644 --- a/src/Equinox.UI.Web/Areas/Identity/IdentityHostingStartup.cs +++ b/src/Equinox.UI.Web/Areas/Identity/IdentityHostingStartup.cs @@ -1,6 +1,4 @@ -using Microsoft.AspNetCore.Hosting; - -[assembly: HostingStartup(typeof(Equinox.UI.Web.Areas.Identity.IdentityHostingStartup))] +[assembly: HostingStartup(typeof(Equinox.UI.Web.Areas.Identity.IdentityHostingStartup))] namespace Equinox.UI.Web.Areas.Identity { public class IdentityHostingStartup : IHostingStartup diff --git a/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 00000000..6354164a --- /dev/null +++ b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,83 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + +

    @ViewData["Title"]

    +
    +
    +
    +
    +

    Use a local account to log in.

    +
    + +
    + + + +
    +
    + + + +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    +

    Use another service to log in.

    +
    + @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
    +

    + There are no external authentication services configured. See this article + about setting up this ASP.NET application to support logging in via external services. +

    +
    + } + else + { +
    +
    +

    + @foreach (var provider in Model.ExternalLogins!) + { + + } +

    +
    +
    + } + } +
    +
    +
    + +@section Scripts { + +} diff --git a/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 00000000..37d2ead7 --- /dev/null +++ b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Equinox.UI.Web.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, + ILogger logger, + UserManager userManager) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList ExternalLogins { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + if (!string.IsNullOrEmpty(ErrorMessage)) + { + ModelState.AddModelError(string.Empty, ErrorMessage); + } + + returnUrl ??= Url.Content("~/"); + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + returnUrl ??= Url.Content("~/"); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + return LocalRedirect(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User account locked out."); + return RedirectToPage("./Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return Page(); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} diff --git a/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml new file mode 100644 index 00000000..cad49d71 --- /dev/null +++ b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml @@ -0,0 +1,21 @@ +@page +@model LogoutModel +@{ + ViewData["Title"] = "Log out"; +} + +
    +

    @ViewData["Title"]

    + @{ + if (User.Identity?.IsAuthenticated ?? false) + { +
    + +
    + } + else + { +

    You have successfully logged out of the application.

    + } + } +
    diff --git a/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs new file mode 100644 index 00000000..ffeec817 --- /dev/null +++ b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Logout.cshtml.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Equinox.UI.Web.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LogoutModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LogoutModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + public void OnGet() + { + } + + public async Task OnPost(string returnUrl = null) + { + await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out."); + if (returnUrl != null) + { + return LocalRedirect(returnUrl); + } + else + { + return RedirectToPage(); + } + } + } +} diff --git a/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Register.cshtml.cs b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Register.cshtml.cs index 52fcec43..097fd1bd 100644 --- a/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/src/Equinox.UI.Web/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -1,18 +1,12 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Security.Claims; using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; namespace Equinox.UI.Web.Areas.Identity.Pages.Account { @@ -22,18 +16,15 @@ public class RegisterModel : PageModel private readonly SignInManager _signInManager; private readonly UserManager _userManager; private readonly ILogger _logger; - private readonly IEmailSender _emailSender; public RegisterModel( UserManager userManager, SignInManager signInManager, - ILogger logger, - IEmailSender emailSender) + ILogger logger) { _userManager = userManager; _signInManager = signInManager; _logger = logger; - _emailSender = emailSender; } [BindProperty] @@ -91,9 +82,6 @@ public async Task OnPostAsync(string returnUrl = null) values: new { area = "Identity", userId = user.Id, code }, protocol: Request.Scheme); - await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", - $"Please confirm your account by clicking here."); - if (_userManager.Options.SignIn.RequireConfirmedAccount) { return RedirectToPage("RegisterConfirmation", new { email = Input.Email }); diff --git a/src/Equinox.UI.Web/Configurations/AutoMapperConfig.cs b/src/Equinox.UI.Web/Configurations/AutoMapperConfig.cs index a62cc018..7ef2217c 100644 --- a/src/Equinox.UI.Web/Configurations/AutoMapperConfig.cs +++ b/src/Equinox.UI.Web/Configurations/AutoMapperConfig.cs @@ -1,16 +1,16 @@ -using System; -using Equinox.Application.AutoMapper; -using Microsoft.Extensions.DependencyInjection; +using Equinox.Application.AutoMapper; namespace Equinox.UI.Web.Configurations { public static class AutoMapperConfig { - public static void AddAutoMapperConfiguration(this IServiceCollection services) + public static WebApplicationBuilder AddAutoMapperConfiguration(this WebApplicationBuilder builder) { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - services.AddAutoMapper(typeof(DomainToViewModelMappingProfile), typeof(ViewModelToDomainMappingProfile)); + builder.Services.AddAutoMapper(typeof(DomainToViewModelMappingProfile), typeof(ViewModelToDomainMappingProfile)); + + return builder; } } } \ No newline at end of file diff --git a/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs b/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs index 0e189cdc..2b8cf404 100644 --- a/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs +++ b/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs @@ -1,22 +1,21 @@ -using System; -using Equinox.Infra.Data.Context; +using Equinox.Infra.Data.Context; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace Equinox.UI.Web.Configurations { public static class DatabaseConfig { - public static void AddDatabaseConfiguration(this IServiceCollection services, IConfiguration configuration) + public static WebApplicationBuilder AddDatabaseConfiguration(this WebApplicationBuilder builder) { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - services.AddDbContext(options => - options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); + builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); - services.AddDbContext(options => - options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); + builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + + return builder; } } } \ No newline at end of file diff --git a/src/Equinox.UI.Web/Configurations/DependencyInjectionConfig.cs b/src/Equinox.UI.Web/Configurations/DependencyInjectionConfig.cs index bdd4dfe0..e518d50a 100644 --- a/src/Equinox.UI.Web/Configurations/DependencyInjectionConfig.cs +++ b/src/Equinox.UI.Web/Configurations/DependencyInjectionConfig.cs @@ -1,16 +1,14 @@ -using System; -using Equinox.Infra.CrossCutting.IoC; -using Microsoft.Extensions.DependencyInjection; +using Equinox.Infra.CrossCutting.IoC; namespace Equinox.UI.Web.Configurations { public static class DependencyInjectionConfig { - public static void AddDependencyInjectionConfiguration(this IServiceCollection services) + public static void AddDependencyInjectionConfiguration(this WebApplicationBuilder builder) { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (builder == null) throw new ArgumentNullException(nameof(builder)); - NativeInjectorBootStrapper.RegisterServices(services); + NativeInjectorBootStrapper.RegisterServices(builder); } } } \ No newline at end of file diff --git a/src/Equinox.UI.Web/Configurations/IdentityConfig.cs b/src/Equinox.UI.Web/Configurations/IdentityConfig.cs index cc58b62f..9780c237 100644 --- a/src/Equinox.UI.Web/Configurations/IdentityConfig.cs +++ b/src/Equinox.UI.Web/Configurations/IdentityConfig.cs @@ -1,27 +1,8 @@ -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Equinox.UI.Web.Configurations +namespace Equinox.UI.Web.Configurations { public static class IdentityConfig { - public static void AddSocialAuthenticationConfiguration(this IServiceCollection services, IConfiguration configuration) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - services.AddAuthentication() - .AddFacebook(o => - { - o.AppId = configuration["Authentication:Facebook:AppId"]; - o.AppSecret = configuration["Authentication:Facebook:AppSecret"]; - }) - .AddGoogle(googleOptions => - { - googleOptions.ClientId = configuration["Authentication:Google:ClientId"]; - googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"]; - }); - } + } } \ No newline at end of file diff --git a/src/Equinox.UI.Web/Configurations/MediatRConfig.cs b/src/Equinox.UI.Web/Configurations/MediatRConfig.cs new file mode 100644 index 00000000..2b3dee57 --- /dev/null +++ b/src/Equinox.UI.Web/Configurations/MediatRConfig.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +namespace Equinox.UI.Web.Configurations +{ + public static class MediatRConfig + { + public static WebApplicationBuilder AddMediatRConfiguration(this WebApplicationBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Equinox.UI.Web/Configurations/MvcConfig.cs b/src/Equinox.UI.Web/Configurations/MvcConfig.cs new file mode 100644 index 00000000..0e7b903c --- /dev/null +++ b/src/Equinox.UI.Web/Configurations/MvcConfig.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Equinox.UI.Web.Configurations +{ + public static class MvcConfig + { + public static WebApplicationBuilder AddMvcConfiguration(this WebApplicationBuilder builder) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + + builder.Configuration + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) + .AddEnvironmentVariables(); + + builder.Services.AddControllersWithViews(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }); + + builder.Services.AddRazorPages(); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Equinox.UI.Web/Controllers/BaseController.cs b/src/Equinox.UI.Web/Controllers/BaseController.cs index fa9f0d22..e0fa8be6 100644 --- a/src/Equinox.UI.Web/Controllers/BaseController.cs +++ b/src/Equinox.UI.Web/Controllers/BaseController.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation.Results; +using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; namespace Equinox.UI.Web.Controllers diff --git a/src/Equinox.UI.Web/Controllers/CustomerController.cs b/src/Equinox.UI.Web/Controllers/CustomerController.cs index a43ef1c3..3cfa5cdb 100644 --- a/src/Equinox.UI.Web/Controllers/CustomerController.cs +++ b/src/Equinox.UI.Web/Controllers/CustomerController.cs @@ -1,10 +1,8 @@ -using System; -using System.Threading.Tasks; using Equinox.Application.Interfaces; using Equinox.Application.ViewModels; +using Equinox.Infra.CrossCutting.Identity.Authorization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using NetDevPack.Identity.Authorization; namespace Equinox.UI.Web.Controllers { diff --git a/src/Equinox.UI.Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/Equinox.UI.Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs index 27e670ea..eaa9ec6c 100644 --- a/src/Equinox.UI.Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs +++ b/src/Equinox.UI.Web/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using System; namespace Equinox.UI.Web.Data.Migrations { diff --git a/src/Equinox.UI.Web/Equinox.UI.Web.csproj b/src/Equinox.UI.Web/Equinox.UI.Web.csproj index f944e223..cb732910 100644 --- a/src/Equinox.UI.Web/Equinox.UI.Web.csproj +++ b/src/Equinox.UI.Web/Equinox.UI.Web.csproj @@ -1,31 +1,30 @@ - net6.0 + net8.0 + disable aspnet-Equinox.UI.Web-61A38C1A-B3EE-4175-AD27-CD2A22786741 enable Linux ..\.. - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + - - - + + + \ No newline at end of file diff --git a/src/Equinox.UI.Web/Program.cs b/src/Equinox.UI.Web/Program.cs index 1657d32c..07667e82 100644 --- a/src/Equinox.UI.Web/Program.cs +++ b/src/Equinox.UI.Web/Program.cs @@ -1,76 +1,38 @@ -using Equinox.Infra.CrossCutting.Identity; using Equinox.UI.Web.Configurations; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using NetDevPack.Identity; -using NetDevPack.Identity.User; +using Equinox.Infra.CrossCutting.Identity.Configuration; var builder = WebApplication.CreateBuilder(args); -builder.Configuration - .SetBasePath(builder.Environment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) - .AddEnvironmentVariables(); - -// ConfigureServices - -builder.Services.AddControllersWithViews(options => -{ - options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); -}); -builder.Services.AddRazorPages(); - -// Setting DBContexts -builder.Services.AddDatabaseConfiguration(builder.Configuration); - -// ASP.NET Identity Settings -builder.Services.AddWebAppIdentityConfiguration(builder.Configuration); - -// Authentication & Authorization -builder.Services.AddSocialAuthenticationConfiguration(builder.Configuration); - -// Interactive AspNetUser (logged in) -// NetDevPack.Identity dependency -builder.Services.AddAspNetUserConfiguration(); - -// AutoMapper Settings -builder.Services.AddAutoMapperConfiguration(); - -// Adding MediatR for Domain Events and Notifications -builder.Services.AddMediatR(AppDomain.CurrentDomain.GetAssemblies()); - -// .NET Native DI Abstraction -builder.Services.AddDependencyInjectionConfiguration(); +// Adding Services +builder.AddMvcConfiguration() // Entire Equinox MVC Config + .AddDatabaseConfiguration() // Setting DBContexts + .AddWebIdentityConfiguration() // ASP.NET Identity Config + .AddAutoMapperConfiguration() // AutoMapper Config + .AddMediatRConfiguration() // Adding MediatR for Domain Events and Notifications + .AddDependencyInjectionConfiguration(); // DotNet Native DI Abstraction var app = builder.Build(); -// Configure - +// Configure Services if (app.Environment.IsDevelopment()) { - app.UseDeveloperExceptionPage(); - app.UseMigrationsEndPoint(); + app.UseDeveloperExceptionPage() + .UseMigrationsEndPoint(); } else { - app.UseExceptionHandler("/error/500"); - app.UseStatusCodePagesWithRedirects("/error/{0}"); - app.UseHsts(); + app.UseExceptionHandler("/error/500") + .UseStatusCodePagesWithRedirects("/error/{0}") + .UseHsts(); } -app.UseHttpsRedirection(); -app.UseStaticFiles(); - -app.UseRouting(); - -// NetDevPack.Identity dependency -app.UseAuthConfiguration(); - -app.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); +app.UseHttpsRedirection() + .UseStaticFiles() + .UseRouting() + .UseAuthentication() + .UseAuthorization(); +app.MapControllerRoute(name: "default",pattern: "{controller=Home}/{action=Index}/{id?}"); app.MapRazorPages(); app.Run(); diff --git a/src/Equinox.UI.Web/Views/Customer/Index.cshtml b/src/Equinox.UI.Web/Views/Customer/Index.cshtml index 9309318f..235dc8e0 100644 --- a/src/Equinox.UI.Web/Views/Customer/Index.cshtml +++ b/src/Equinox.UI.Web/Views/Customer/Index.cshtml @@ -123,7 +123,7 @@ $(".viewbutton").on("click", function() { var customerId = $(this).data('id'); $.ajax({ - url: "https://localhost:44314/customer-management/customer-history/" + customerId, + url: "https://localhost:5001/customer-management/customer-history/" + customerId, //url: "http://equinoxproject.azurewebsites.net/customer-management/customer-history/" + customerId, cache: false }).done(function(data) { diff --git a/src/Equinox.UI.Web/Views/Home/Index.cshtml b/src/Equinox.UI.Web/Views/Home/Index.cshtml index c4e5a6c1..ee966bf9 100644 --- a/src/Equinox.UI.Web/Views/Home/Index.cshtml +++ b/src/Equinox.UI.Web/Views/Home/Index.cshtml @@ -20,11 +20,11 @@

    Technologies

      -
    • .NET 6.0
    • -
    • ASP.NET MVC 6.0
    • -
    • ASP.NET WebAPI 6.0
    • -
    • ASP.NET Identity 6.0
    • -
    • EF Core 6.0
    • +
    • .NET 8.0
    • +
    • ASP.NET MVC 8.0
    • +
    • ASP.NET WebAPI 8.0
    • +
    • ASP.NET Identity 8.0
    • +
    • EF Core 8.0
    • AutoMapper
    • FluentValidator
    • MediatR
    • diff --git a/src/Equinox.UI.Web/wwwroot/images/banner1.svg b/src/Equinox.UI.Web/wwwroot/images/banner1.svg index ebd3bfe5..e17e7208 100644 --- a/src/Equinox.UI.Web/wwwroot/images/banner1.svg +++ b/src/Equinox.UI.Web/wwwroot/images/banner1.svg @@ -13,7 +13,7 @@ Run your application anywhere - ASP.NET 6.0 + ASP.NET 8.0 From 89febfa173e2bdd35938108acf41974d95934c66 Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Wed, 31 Jul 2024 15:35:22 -0300 Subject: [PATCH 07/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca6d4350..ad8b5c30 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The Equinox Project is a open-source project written in .NET Core The goal of this project is implement the most common used technologies and share with the technical community the best way to develop great applications with .NET -[![Build status](https://ci.appveyor.com/api/projects/status/rl2ja69994rt3ei6?svg=true)](https://ci.appveyor.com/project/EduardoPires/equinoxproject) +![Build Status](https://github.com/EduardoPires/EquinoxProject/actions/workflows/dotnet-core.yml/badge.svg) [![License](https://img.shields.io/github/license/eduardopires/equinoxproject.svg)](LICENSE) [![Issues open](https://img.shields.io/github/issues/eduardopires/equinoxproject.svg)](https://huboard.com/EduardoPires/EquinoxProject/) From 6d55bf020d9a89e99f13f876bb44c67507adc7e1 Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Thu, 1 Aug 2024 18:48:45 -0300 Subject: [PATCH 08/21] Architecture Testing --- Equinox.sln | 31 +- docs/Architecture.dgml | 503 ++++++++++++++++++ src/Equinox.Domain.Core/Models/ValueObject.cs | 36 -- .../Controllers/BaseController.cs | 2 +- .../DataBaseTests.cs | 37 ++ .../Equinox.Tests.Architecture/DomainTests.cs | 75 +++ .../Equinox.Tests.Architecture.csproj | 36 ++ .../GeneralPatternTests.cs | 46 ++ .../ShouldUseDependencyInjectionRule.cs | 27 + .../Support/TestOutputHelperTextWriter.cs | 21 + .../Support/TestsSupport.cs | 30 ++ .../WebApplicationTests.cs | 36 ++ 12 files changed, 832 insertions(+), 48 deletions(-) create mode 100644 docs/Architecture.dgml delete mode 100644 src/Equinox.Domain.Core/Models/ValueObject.cs create mode 100644 tests/Equinox.Tests.Architecture/DataBaseTests.cs create mode 100644 tests/Equinox.Tests.Architecture/DomainTests.cs create mode 100644 tests/Equinox.Tests.Architecture/Equinox.Tests.Architecture.csproj create mode 100644 tests/Equinox.Tests.Architecture/GeneralPatternTests.cs create mode 100644 tests/Equinox.Tests.Architecture/Support/ShouldUseDependencyInjectionRule.cs create mode 100644 tests/Equinox.Tests.Architecture/Support/TestOutputHelperTextWriter.cs create mode 100644 tests/Equinox.Tests.Architecture/Support/TestsSupport.cs create mode 100644 tests/Equinox.Tests.Architecture/WebApplicationTests.cs diff --git a/Equinox.sln b/Equinox.sln index 91228308..bc6aaf89 100644 --- a/Equinox.sln +++ b/Equinox.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31025.218 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1 - Presentation", "1 - Presentation", "{BA4C44CC-65BC-4CE0-9B44-68BA231FAC73}" EndProject @@ -17,23 +17,27 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.1 - Data", "5.1 - Data", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.2 - CrossCutting", "5.2 - CrossCutting", "{DF6C4BDE-F3C5-4E53-A5D5-9D27B2D3E38F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.UI.Web", "src\Equinox.UI.Web\Equinox.UI.Web.csproj", "{490517BA-F3C3-44B4-82F8-1E3A4ED6777A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.UI.Web", "src\Equinox.UI.Web\Equinox.UI.Web.csproj", "{490517BA-F3C3-44B4-82F8-1E3A4ED6777A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Services.Api", "src\Equinox.Services.Api\Equinox.Services.Api.csproj", "{EA966EC3-85A6-4B57-82C1-5120E3390243}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Services.Api", "src\Equinox.Services.Api\Equinox.Services.Api.csproj", "{EA966EC3-85A6-4B57-82C1-5120E3390243}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Application", "src\Equinox.Application\Equinox.Application.csproj", "{851E7338-2397-4E8D-8199-FD1EAD109418}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Application", "src\Equinox.Application\Equinox.Application.csproj", "{851E7338-2397-4E8D-8199-FD1EAD109418}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Domain", "src\Equinox.Domain\Equinox.Domain.csproj", "{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Domain", "src\Equinox.Domain\Equinox.Domain.csproj", "{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Domain.Core", "src\Equinox.Domain.Core\Equinox.Domain.Core.csproj", "{9CC884B7-FA70-49E1-92A6-2B566E00FAB9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Domain.Core", "src\Equinox.Domain.Core\Equinox.Domain.Core.csproj", "{9CC884B7-FA70-49E1-92A6-2B566E00FAB9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Infra.Data", "src\Equinox.Infra.Data\Equinox.Infra.Data.csproj", "{F0DDF87D-98A4-4237-91C9-FD865ED78ABB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Infra.Data", "src\Equinox.Infra.Data\Equinox.Infra.Data.csproj", "{F0DDF87D-98A4-4237-91C9-FD865ED78ABB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Infra.CrossCutting.Bus", "src\Equinox.Infra.CrossCutting.Bus\Equinox.Infra.CrossCutting.Bus.csproj", "{91F0D76D-2BEA-43D2-B123-DC29F2E87792}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Infra.CrossCutting.Bus", "src\Equinox.Infra.CrossCutting.Bus\Equinox.Infra.CrossCutting.Bus.csproj", "{91F0D76D-2BEA-43D2-B123-DC29F2E87792}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Infra.CrossCutting.Identity", "src\Equinox.Infra.CrossCutting.Identity\Equinox.Infra.CrossCutting.Identity.csproj", "{788030D0-561B-4136-96B4-D5ABFB5CFD07}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Infra.CrossCutting.Identity", "src\Equinox.Infra.CrossCutting.Identity\Equinox.Infra.CrossCutting.Identity.csproj", "{788030D0-561B-4136-96B4-D5ABFB5CFD07}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Infra.CrossCutting.IoC", "src\Equinox.Infra.CrossCutting.IoC\Equinox.Infra.CrossCutting.IoC.csproj", "{B3000AD2-5EAA-49A2-8FC4-10DF329B15B0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Infra.CrossCutting.IoC", "src\Equinox.Infra.CrossCutting.IoC\Equinox.Infra.CrossCutting.IoC.csproj", "{B3000AD2-5EAA-49A2-8FC4-10DF329B15B0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6 - Tests", "6 - Tests", "{7426F6BA-3DAD-411E-9956-3980D42DC36F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equinox.Tests.Architecture", "tests\Equinox.Tests.Architecture\Equinox.Tests.Architecture.csproj", "{C4036D98-A669-440C-9970-05DB45815949}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -77,6 +81,10 @@ Global {B3000AD2-5EAA-49A2-8FC4-10DF329B15B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3000AD2-5EAA-49A2-8FC4-10DF329B15B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {B3000AD2-5EAA-49A2-8FC4-10DF329B15B0}.Release|Any CPU.Build.0 = Release|Any CPU + {C4036D98-A669-440C-9970-05DB45815949}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4036D98-A669-440C-9970-05DB45815949}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4036D98-A669-440C-9970-05DB45815949}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4036D98-A669-440C-9970-05DB45815949}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,6 +101,7 @@ Global {91F0D76D-2BEA-43D2-B123-DC29F2E87792} = {DF6C4BDE-F3C5-4E53-A5D5-9D27B2D3E38F} {788030D0-561B-4136-96B4-D5ABFB5CFD07} = {DF6C4BDE-F3C5-4E53-A5D5-9D27B2D3E38F} {B3000AD2-5EAA-49A2-8FC4-10DF329B15B0} = {DF6C4BDE-F3C5-4E53-A5D5-9D27B2D3E38F} + {C4036D98-A669-440C-9970-05DB45815949} = {7426F6BA-3DAD-411E-9956-3980D42DC36F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4050E145-6791-440A-A2E5-75B05ACD4AFE} diff --git a/docs/Architecture.dgml b/docs/Architecture.dgml new file mode 100644 index 00000000..025b9a8f --- /dev/null +++ b/docs/Architecture.dgml @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Equinox.Domain.Core/Models/ValueObject.cs b/src/Equinox.Domain.Core/Models/ValueObject.cs deleted file mode 100644 index 5ea96d73..00000000 --- a/src/Equinox.Domain.Core/Models/ValueObject.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Equinox.Domain.Core.Models -{ - public abstract class ValueObject where T : ValueObject - { - public override bool Equals(object obj) - { - var valueObject = obj as T; - return EqualsCore(valueObject); - } - - protected abstract bool EqualsCore(T other); - - public override int GetHashCode() - { - return GetHashCodeCore(); - } - - protected abstract int GetHashCodeCore(); - - public static bool operator ==(ValueObject a, ValueObject b) - { - if (a is null && b is null) - return true; - - if (a is null || b is null) - return false; - - return a.Equals(b); - } - - public static bool operator !=(ValueObject a, ValueObject b) - { - return !(a == b); - } - } -} \ No newline at end of file diff --git a/src/Equinox.UI.Web/Controllers/BaseController.cs b/src/Equinox.UI.Web/Controllers/BaseController.cs index e0fa8be6..5110a575 100644 --- a/src/Equinox.UI.Web/Controllers/BaseController.cs +++ b/src/Equinox.UI.Web/Controllers/BaseController.cs @@ -3,7 +3,7 @@ namespace Equinox.UI.Web.Controllers { - public class BaseController : Controller + public abstract class BaseController : Controller { private readonly ICollection _errors = new List(); diff --git a/tests/Equinox.Tests.Architecture/DataBaseTests.cs b/tests/Equinox.Tests.Architecture/DataBaseTests.cs new file mode 100644 index 00000000..8bfc24d1 --- /dev/null +++ b/tests/Equinox.Tests.Architecture/DataBaseTests.cs @@ -0,0 +1,37 @@ +using Equinox.Tests.Architecture.Support; +using NetArchTest.Rules; +using Xunit.Abstractions; + +namespace Equinox.Tests.Architecture; + +public class DataBaseTests +{ + private readonly ITestOutputHelper _output; + + public DataBaseTests(ITestOutputHelper output) + { + _output = output; + Console.SetOut(new TestOutputHelperTextWriter(_output)); + } + + + [Fact(DisplayName = "Repository Classes Must Have Constructor With Parameters")] + [Trait("", "Database")] + public void RepositoryClasses_MustHave_ConstructorWithParameters() + { + // Arrange + var assemblies = TestsSupport.GetAllProjectAssemblies(); + + var repositories = Types.InAssemblies(assemblies) + .That().AreClasses() + .And().HaveNameEndingWith("Repository"); + + // Act + var result = repositories + .Should() + .MeetCustomRule(new ShouldUseDependencyInjectionRule()) + .GetResult(); + + Assert.True(result.IsSuccessful, $"Failed in {result.FailingTypes?.Count} objects(s)"); + } +} diff --git a/tests/Equinox.Tests.Architecture/DomainTests.cs b/tests/Equinox.Tests.Architecture/DomainTests.cs new file mode 100644 index 00000000..6a70d2d4 --- /dev/null +++ b/tests/Equinox.Tests.Architecture/DomainTests.cs @@ -0,0 +1,75 @@ +using Equinox.Domain.Core.Events; +using Equinox.Domain.Models; +using NetArchTest.Rules; + +namespace Equinox.Tests.Architecture; + +public class DomainTests +{ + [Fact(DisplayName = "Domain Should Not Have Any Dependencies")] + [Trait("", "Domain")] + public void Domain_ShouldNotHave_ProjectDependencies() + { + // Arrange + var domain = Types.InAssembly(typeof(Customer).Assembly); + + // Act + var result = domain + .ShouldNot() + .HaveDependencyOnAny + ("Equinox.UI.Web", + "Equinox.Services.Api", + "Equinox.Application", + "Equinox.Infra.Data", + "Equinox.Domain.Core", + "Equinox.Infra.CrossCutting.Bus", + "Equinox.Infra.CrossCutting.Identity", + "Equinox.Infra.CrossCutting.IoC") + .GetResult(); + + // Assert + Assert.True(result.IsSuccessful, $"Failed in {result.FailingTypes?.Count} dependencie(s)"); + } + + [Fact(DisplayName = "Domain Namespace Should Have A Pattern")] + [Trait("", "Domain")] + public void DomainElements_MustReside_InSameNameSpace() + { + // Arrange + var domain = Types.InAssembly(typeof(Customer).Assembly); + + // Act + var result = domain + .Should() + .ResideInNamespaceStartingWith("Equinox.Domain") + .GetResult(); + + // Assert + Assert.True(result.IsSuccessful, $"Failed in {result.FailingTypes?.Count} type(s)"); + } + + [Fact(DisplayName = "Domain Core Should Not Have Any Dependencies")] + [Trait("", "Domain")] + public void DomainCore_ShouldNotHave_ProjectDependencies() + { + // Arrange + var domainCore = Types.InAssembly(typeof(StoredEvent).Assembly); + + // Act + var result = domainCore + .ShouldNot() + .HaveDependencyOnAny + ("Equinox.UI.Web", + "Equinox.Services.Api", + "Equinox.Application", + "Equinox.Infra.Data", + "Equinox.Domain", + "Equinox.Infra.CrossCutting.Bus", + "Equinox.Infra.CrossCutting.Identity", + "Equinox.Infra.CrossCutting.IoC") + .GetResult(); + + // Assert + Assert.True(result.IsSuccessful, $"Failed in {result.FailingTypes?.Count} dependencie(s)"); + } +} diff --git a/tests/Equinox.Tests.Architecture/Equinox.Tests.Architecture.csproj b/tests/Equinox.Tests.Architecture/Equinox.Tests.Architecture.csproj new file mode 100644 index 00000000..ec3390e2 --- /dev/null +++ b/tests/Equinox.Tests.Architecture/Equinox.Tests.Architecture.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Equinox.Tests.Architecture/GeneralPatternTests.cs b/tests/Equinox.Tests.Architecture/GeneralPatternTests.cs new file mode 100644 index 00000000..45ccd930 --- /dev/null +++ b/tests/Equinox.Tests.Architecture/GeneralPatternTests.cs @@ -0,0 +1,46 @@ +using Equinox.Tests.Architecture.Support; +using NetArchTest.Rules; + +namespace Equinox.Tests.Architecture; + +public class GeneralPatternTests +{ + [Fact(DisplayName = "Base Classes Must Be Abstract")] + [Trait("", "General")] + public void BaseClasses_MustBe_Abstract() + { + // Arrange + var assemblies = TestsSupport.GetAllProjectAssemblies(); + + var baseClasses = Types.InAssemblies(assemblies) + .That().AreClasses() + .And().HaveNameStartingWith("Base"); + + // Act + var result = baseClasses.Should() + .BeAbstract() + .GetResult(); + + // Assert + Assert.True(result.IsSuccessful, $"Failed in {result.FailingTypes?.Count} objects(s)"); + } + + [Fact(DisplayName = "Interfaces Must Starts With I")] + [Trait("", "General")] + public void Interfaces_MustStarts_WithI() + { + // Arrange + var assemblies = TestsSupport.GetAllProjectAssemblies(); + + var interfaces = Types.InAssemblies(assemblies) + .That().AreInterfaces(); + + // Act + var result = interfaces.Should() + .HaveNameStartingWith("I") + .GetResult(); + + // Assert + Assert.True(result.IsSuccessful, $"Failed in {result.FailingTypes?.Count} objects(s)"); + } +} diff --git a/tests/Equinox.Tests.Architecture/Support/ShouldUseDependencyInjectionRule.cs b/tests/Equinox.Tests.Architecture/Support/ShouldUseDependencyInjectionRule.cs new file mode 100644 index 00000000..c3e2816a --- /dev/null +++ b/tests/Equinox.Tests.Architecture/Support/ShouldUseDependencyInjectionRule.cs @@ -0,0 +1,27 @@ +using Mono.Cecil; +using NetArchTest.Rules; + +namespace Equinox.Tests.Architecture.Support; + +public class ShouldUseDependencyInjectionRule : ICustomRule +{ + public bool MeetsRule(TypeDefinition type) + { + if (!type.IsClass || !type.IsPublic) + { + return true; // Ignore non-public classes or non-classes + } + + foreach (var method in type.Methods) + { + if (method.IsConstructor && method.HasParameters) + { + Console.WriteLine($"The class {type.Name} has a constructor with {method.Parameters.Count()} parameters"); + return true; + } + } + + // If we found no constructors with parameters, assume the class does not use DI + return false; + } +} diff --git a/tests/Equinox.Tests.Architecture/Support/TestOutputHelperTextWriter.cs b/tests/Equinox.Tests.Architecture/Support/TestOutputHelperTextWriter.cs new file mode 100644 index 00000000..f29c4a16 --- /dev/null +++ b/tests/Equinox.Tests.Architecture/Support/TestOutputHelperTextWriter.cs @@ -0,0 +1,21 @@ +using System.Text; +using Xunit.Abstractions; + +namespace Equinox.Tests.Architecture.Support; + +public class TestOutputHelperTextWriter : TextWriter +{ + private readonly ITestOutputHelper _output; + + public TestOutputHelperTextWriter(ITestOutputHelper output) + { + _output = output; + } + + public override void WriteLine(string value) + { + _output.WriteLine(value); + } + + public override Encoding Encoding => Encoding.UTF8; +} diff --git a/tests/Equinox.Tests.Architecture/Support/TestsSupport.cs b/tests/Equinox.Tests.Architecture/Support/TestsSupport.cs new file mode 100644 index 00000000..8ca2e145 --- /dev/null +++ b/tests/Equinox.Tests.Architecture/Support/TestsSupport.cs @@ -0,0 +1,30 @@ +using Equinox.Application.Services; +using Equinox.Domain.Core.Events; +using Equinox.Domain.Models; +using Equinox.Infra.CrossCutting.Bus; +using Equinox.Infra.CrossCutting.Identity.User; +using Equinox.Infra.CrossCutting.IoC; +using Equinox.Infra.Data.Context; +using Equinox.Services.Api.Controllers; +using Equinox.UI.Web.Controllers; +using System.Reflection; + +namespace Equinox.Tests.Architecture.Support; + +public class TestsSupport +{ + public static IEnumerable GetAllProjectAssemblies() + { + IEnumerable assemblies = [typeof(HomeController).Assembly, + typeof(AccountController).Assembly, + typeof(CustomerAppService).Assembly, + typeof(Customer).Assembly, + typeof(StoredEvent).Assembly, + typeof(EquinoxContext).Assembly, + typeof(InMemoryBus).Assembly, + typeof(AspNetUser).Assembly, + typeof(NativeInjectorBootStrapper).Assembly]; + + return assemblies; + } +} diff --git a/tests/Equinox.Tests.Architecture/WebApplicationTests.cs b/tests/Equinox.Tests.Architecture/WebApplicationTests.cs new file mode 100644 index 00000000..4da3e356 --- /dev/null +++ b/tests/Equinox.Tests.Architecture/WebApplicationTests.cs @@ -0,0 +1,36 @@ +using Equinox.Application.Services; +using Equinox.Domain.Core.Events; +using Equinox.Domain.Models; +using Equinox.Infra.CrossCutting.Bus; +using Equinox.Infra.CrossCutting.Identity.User; +using Equinox.Infra.CrossCutting.IoC; +using Equinox.Infra.Data.Context; +using Equinox.Services.Api.Controllers; +using Equinox.UI.Web.Controllers; +using NetArchTest.Rules; +using NetArchTest.Rules.Policies; +using System.Reflection; + +namespace Equinox.Tests.Architecture; + +public class WebApplicationTests +{ + [Fact(DisplayName = "Web Application Should Not Have Data Dependencies")] + [Trait("", "Web Application")] + public void Application_ShouldNotHave_DataAccessDependencies() + { + // Arrange + var appWeb = Types.InAssembly(typeof(HomeController).Assembly) + .That().DoNotResideInNamespace("Equinox.UI.Web.Configurations"); + + // Act + var result = appWeb + .ShouldNot() + .HaveDependencyOnAny + ("Equinox.Infra.Data") + .GetResult(); + + // Assert + Assert.True(result.IsSuccessful, $"Failed in {result.FailingTypes?.Count} dependencie(s)"); + } +} From e83638c96c621a8dac1de18ca1f8b35e9b880e1c Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Fri, 2 Aug 2024 13:39:37 -0300 Subject: [PATCH 09/21] Improvements --- .../WebApplicationTests.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/Equinox.Tests.Architecture/WebApplicationTests.cs b/tests/Equinox.Tests.Architecture/WebApplicationTests.cs index 4da3e356..28da12e6 100644 --- a/tests/Equinox.Tests.Architecture/WebApplicationTests.cs +++ b/tests/Equinox.Tests.Architecture/WebApplicationTests.cs @@ -1,15 +1,5 @@ -using Equinox.Application.Services; -using Equinox.Domain.Core.Events; -using Equinox.Domain.Models; -using Equinox.Infra.CrossCutting.Bus; -using Equinox.Infra.CrossCutting.Identity.User; -using Equinox.Infra.CrossCutting.IoC; -using Equinox.Infra.Data.Context; -using Equinox.Services.Api.Controllers; -using Equinox.UI.Web.Controllers; +using Equinox.UI.Web.Controllers; using NetArchTest.Rules; -using NetArchTest.Rules.Policies; -using System.Reflection; namespace Equinox.Tests.Architecture; From 0a825771958cdb5972d7e87b40e5fd31dd924dbf Mon Sep 17 00:00:00 2001 From: Eduardo Pires Date: Tue, 8 Apr 2025 01:05:06 -0300 Subject: [PATCH 10/21] fix: automatic migrations --- .../Equinox.Application.csproj | 4 +- .../Equinox.Domain.Core.csproj | 2 +- src/Equinox.Domain/Equinox.Domain.csproj | 4 +- .../Equinox.Infra.CrossCutting.Bus.csproj | 4 +- .../Configuration/AspNetIdentityConfig.cs | 13 +- .../Data/EquinoxIdentityContext.cs | 33 +++ ...Equinox.Infra.CrossCutting.Identity.csproj | 19 +- .../20250408033115_SQLite.Designer.cs | 268 ++++++++++++++++++ .../Migrations/20250408033115_SQLite.cs | 222 +++++++++++++++ .../EquinoxIdentityContextModelSnapshot.cs | 265 +++++++++++++++++ .../Equinox.Infra.CrossCutting.IoC.csproj | 2 +- .../Equinox.Infra.Data.csproj | 17 +- ...r.cs => 20250408031104_SQLite.Designer.cs} | 32 ++- ...31_Initial.cs => 20250408031104_SQLite.cs} | 15 +- .../Migrations/EquinoxContextModelSnapshot.cs | 27 +- .../20161213130520_Initial.Designer.cs | 44 --- .../EventStoreSQLContextModelSnapshot.cs | 36 ++- .../20250408031128_SQLite.Designer.cs | 53 ++++ .../20250408031128_SQLite.cs} | 19 +- .../Equinox.Services.Api.csproj | 20 +- .../Configurations/DatabaseConfig.cs | 23 ++ .../Configurations/DbMigrationHelpers.cs | 99 +++++++ .../Configurations/IdentityConfig.cs | 8 - src/Equinox.UI.Web/Equinox.UI.Web.csproj | 22 +- src/Equinox.UI.Web/EquinoxProject.db | Bin 0 -> 4096 bytes src/Equinox.UI.Web/EquinoxProject.db-shm | Bin 0 -> 32768 bytes src/Equinox.UI.Web/EquinoxProject.db-wal | Bin 0 -> 234872 bytes src/Equinox.UI.Web/Program.cs | 3 + .../Properties/launchSettings.json | 4 +- .../Views/Customer/Index.cshtml | 20 +- .../appsettings.Development.json | 3 +- src/Equinox.UI.Web/appsettings.Staging.json | 1 - src/Equinox.UI.Web/appsettings.Testing.json | 1 - .../Equinox.Tests.Architecture.csproj | 16 +- 34 files changed, 1120 insertions(+), 179 deletions(-) create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.Designer.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.cs create mode 100644 src/Equinox.Infra.CrossCutting.Identity/Migrations/EquinoxIdentityContextModelSnapshot.cs rename src/Equinox.Infra.Data/Migrations/{20161213130431_Initial.Designer.cs => 20250408031104_SQLite.Designer.cs} (55%) rename src/Equinox.Infra.Data/Migrations/{20161213130431_Initial.cs => 20250408031104_SQLite.cs} (67%) delete mode 100644 src/Equinox.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.Designer.cs create mode 100644 src/Equinox.Infra.Data/Migrations/EventStoreSql/20250408031128_SQLite.Designer.cs rename src/Equinox.Infra.Data/Migrations/{EventStoreSQL/20161213130520_Initial.cs => EventStoreSql/20250408031128_SQLite.cs} (55%) create mode 100644 src/Equinox.UI.Web/Configurations/DbMigrationHelpers.cs delete mode 100644 src/Equinox.UI.Web/Configurations/IdentityConfig.cs create mode 100644 src/Equinox.UI.Web/EquinoxProject.db create mode 100644 src/Equinox.UI.Web/EquinoxProject.db-shm create mode 100644 src/Equinox.UI.Web/EquinoxProject.db-wal diff --git a/src/Equinox.Application/Equinox.Application.csproj b/src/Equinox.Application/Equinox.Application.csproj index b3df6541..0f48314a 100644 --- a/src/Equinox.Application/Equinox.Application.csproj +++ b/src/Equinox.Application/Equinox.Application.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 disable @@ -8,6 +8,6 @@ - + \ No newline at end of file diff --git a/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj b/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj index f4bfdf36..cef134b0 100644 --- a/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj +++ b/src/Equinox.Domain.Core/Equinox.Domain.Core.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 disable diff --git a/src/Equinox.Domain/Equinox.Domain.csproj b/src/Equinox.Domain/Equinox.Domain.csproj index f7a018b6..15b9fdb6 100644 --- a/src/Equinox.Domain/Equinox.Domain.csproj +++ b/src/Equinox.Domain/Equinox.Domain.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 disable @@ -8,7 +8,7 @@ - + diff --git a/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj b/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj index 01715596..96090edf 100644 --- a/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj +++ b/src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj @@ -1,10 +1,10 @@ - net8.0 + net9.0 disable - + diff --git a/src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs b/src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs index 16b342ad..369c1f25 100644 --- a/src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs +++ b/src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using System; using System.Text; @@ -38,9 +39,17 @@ public static WebApplicationBuilder AddWebIdentityConfiguration(this WebApplicat private static WebApplicationBuilder AddIdentityDbContext(this WebApplicationBuilder builder) { + if (builder.Environment.IsDevelopment()) + { + + builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); + + return builder; + } + builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), - b => b.MigrationsAssembly("Equinox.Infra.CrossCutting.Identity.Data"))); + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); return builder; } diff --git a/src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs b/src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs index eacd8e71..999d781f 100644 --- a/src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs +++ b/src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs @@ -1,5 +1,9 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using System.IO; +using System; namespace Equinox.Infra.CrossCutting.Identity.Data { @@ -7,4 +11,33 @@ public class EquinoxIdentityContext : IdentityDbContext { public EquinoxIdentityContext(DbContextOptions options) : base(options) { } } + + public class EquinoxIdentityContextFactory : IDesignTimeDbContextFactory + { + public EquinoxIdentityContext CreateDbContext(string[] args) + { + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile($"appsettings.{environment}.json", optional: true) + .Build(); + + var builder = new DbContextOptionsBuilder(); + + var connectionString = configuration.GetConnectionString("DefaultConnection"); + + if (environment == "Development") + { + builder.UseSqlite(connectionString); + } + else + { + builder.UseSqlServer(connectionString); + } + + return new EquinoxIdentityContext(builder.Options); + } + } } \ No newline at end of file diff --git a/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj b/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj index fce7630a..effa8d87 100644 --- a/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj +++ b/src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj @@ -1,20 +1,21 @@  - net8.0 + net9.0 disable - - - - - - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.Designer.cs b/src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.Designer.cs new file mode 100644 index 00000000..b69eadbf --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.Designer.cs @@ -0,0 +1,268 @@ +// +using System; +using Equinox.Infra.CrossCutting.Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Equinox.Infra.CrossCutting.Identity.Migrations +{ + [DbContext(typeof(EquinoxIdentityContext))] + [Migration("20250408033115_SQLite")] + partial class SQLite + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.cs b/src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.cs new file mode 100644 index 00000000..6b943d96 --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.cs @@ -0,0 +1,222 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Equinox.Infra.CrossCutting.Identity.Migrations +{ + /// + public partial class SQLite : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Equinox.Infra.CrossCutting.Identity/Migrations/EquinoxIdentityContextModelSnapshot.cs b/src/Equinox.Infra.CrossCutting.Identity/Migrations/EquinoxIdentityContextModelSnapshot.cs new file mode 100644 index 00000000..ffea074a --- /dev/null +++ b/src/Equinox.Infra.CrossCutting.Identity/Migrations/EquinoxIdentityContextModelSnapshot.cs @@ -0,0 +1,265 @@ +// +using System; +using Equinox.Infra.CrossCutting.Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Equinox.Infra.CrossCutting.Identity.Migrations +{ + [DbContext(typeof(EquinoxIdentityContext))] + partial class EquinoxIdentityContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj b/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj index e7606282..282c7371 100644 --- a/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj +++ b/src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 disable diff --git a/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj b/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj index cd1b3a85..287f0346 100644 --- a/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj +++ b/src/Equinox.Infra.Data/Equinox.Infra.Data.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 disable @@ -8,12 +8,15 @@ - - - + + + - - - + + + + + + \ No newline at end of file diff --git a/src/Equinox.Infra.Data/Migrations/20161213130431_Initial.Designer.cs b/src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.Designer.cs similarity index 55% rename from src/Equinox.Infra.Data/Migrations/20161213130431_Initial.Designer.cs rename to src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.Designer.cs index 6244cea2..eaeb01c7 100644 --- a/src/Equinox.Infra.Data/Migrations/20161213130431_Initial.Designer.cs +++ b/src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.Designer.cs @@ -1,44 +1,50 @@ -using System; +// +using System; +using Equinox.Infra.Data.Context; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using Equinox.Infra.Data.Context; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable namespace Equinox.Infra.Data.Migrations { [DbContext(typeof(EquinoxContext))] - [Migration("20161213130431_Initial")] - partial class Initial + [Migration("20250408031104_SQLite")] + partial class SQLite { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); modelBuilder.Entity("Equinox.Domain.Models.Customer", b => { b.Property("Id") .ValueGeneratedOnAdd() + .HasColumnType("TEXT") .HasColumnName("Id"); - b.Property("BirthDate"); + b.Property("BirthDate") + .HasColumnType("TEXT"); b.Property("Email") .IsRequired() - .HasColumnType("varchar(100)") - .HasMaxLength(11); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.HasKey("Id"); b.ToTable("Customers"); }); +#pragma warning restore 612, 618 } } } diff --git a/src/Equinox.Infra.Data/Migrations/20161213130431_Initial.cs b/src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.cs similarity index 67% rename from src/Equinox.Infra.Data/Migrations/20161213130431_Initial.cs rename to src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.cs index c972c1d0..98241cd0 100644 --- a/src/Equinox.Infra.Data/Migrations/20161213130431_Initial.cs +++ b/src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.cs @@ -1,20 +1,24 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; +#nullable disable + namespace Equinox.Infra.Data.Migrations { - public partial class Initial : Migration + /// + public partial class SQLite : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Customers", columns: table => new { - Id = table.Column(nullable: false), - BirthDate = table.Column(nullable: false), - Email = table.Column(type: "varchar(100)", maxLength: 11, nullable: false), - Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false) + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + Email = table.Column(type: "varchar(100)", maxLength: 100, nullable: false), + BirthDate = table.Column(type: "TEXT", nullable: false) }, constraints: table => { @@ -22,6 +26,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/src/Equinox.Infra.Data/Migrations/EquinoxContextModelSnapshot.cs b/src/Equinox.Infra.Data/Migrations/EquinoxContextModelSnapshot.cs index 2d4e9a17..e6371a74 100644 --- a/src/Equinox.Infra.Data/Migrations/EquinoxContextModelSnapshot.cs +++ b/src/Equinox.Infra.Data/Migrations/EquinoxContextModelSnapshot.cs @@ -1,8 +1,11 @@ -using System; +// +using System; +using Equinox.Infra.Data.Context; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Equinox.Infra.Data.Context; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable namespace Equinox.Infra.Data.Migrations { @@ -11,32 +14,34 @@ partial class EquinoxContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); modelBuilder.Entity("Equinox.Domain.Models.Customer", b => { b.Property("Id") .ValueGeneratedOnAdd() + .HasColumnType("TEXT") .HasColumnName("Id"); - b.Property("BirthDate"); + b.Property("BirthDate") + .HasColumnType("TEXT"); b.Property("Email") .IsRequired() - .HasColumnType("varchar(100)") - .HasMaxLength(11); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("Name") .IsRequired() - .HasColumnType("varchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.HasKey("Id"); b.ToTable("Customers"); }); +#pragma warning restore 612, 618 } } } diff --git a/src/Equinox.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.Designer.cs b/src/Equinox.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.Designer.cs deleted file mode 100644 index 9bed57c4..00000000 --- a/src/Equinox.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.Designer.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Equinox.Infra.Data.Context; - -namespace Equinox.Infra.Data.Migrations.EventStoreSQL -{ - [DbContext(typeof(EventStoreSqlContext))] - [Migration("20161213130520_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("Equinox.Domain.Core.Events.StoredEvent", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("AggregateId"); - - b.Property("Data"); - - b.Property("MessageType") - .HasColumnName("Action") - .HasColumnType("varchar(100)"); - - b.Property("Timestamp") - .HasColumnName("CreationDate"); - - b.Property("User"); - - b.HasKey("Id"); - - b.ToTable("StoredEvent"); - }); - } - } -} diff --git a/src/Equinox.Infra.Data/Migrations/EventStoreSQL/EventStoreSQLContextModelSnapshot.cs b/src/Equinox.Infra.Data/Migrations/EventStoreSQL/EventStoreSQLContextModelSnapshot.cs index d156d92d..530d0ee8 100644 --- a/src/Equinox.Infra.Data/Migrations/EventStoreSQL/EventStoreSQLContextModelSnapshot.cs +++ b/src/Equinox.Infra.Data/Migrations/EventStoreSQL/EventStoreSQLContextModelSnapshot.cs @@ -1,42 +1,50 @@ -using System; +// +using System; +using Equinox.Infra.Data.Context; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Equinox.Infra.Data.Context; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable -namespace Equinox.Infra.Data.Migrations.EventStoreSQL +namespace Equinox.Infra.Data.Migrations.EventStoreSql { [DbContext(typeof(EventStoreSqlContext))] - partial class EventStoreSQLContextModelSnapshot : ModelSnapshot + partial class EventStoreSqlContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.0-rtm-22752") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); modelBuilder.Entity("Equinox.Domain.Core.Events.StoredEvent", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); - b.Property("AggregateId"); + b.Property("AggregateId") + .HasColumnType("TEXT"); - b.Property("Data"); + b.Property("Data") + .HasColumnType("TEXT"); b.Property("MessageType") - .HasColumnName("Action") - .HasColumnType("varchar(100)"); + .HasColumnType("varchar(100)") + .HasColumnName("Action"); b.Property("Timestamp") + .HasColumnType("TEXT") .HasColumnName("CreationDate"); - b.Property("User"); + b.Property("User") + .HasColumnType("TEXT"); b.HasKey("Id"); b.ToTable("StoredEvent"); }); +#pragma warning restore 612, 618 } } } diff --git a/src/Equinox.Infra.Data/Migrations/EventStoreSql/20250408031128_SQLite.Designer.cs b/src/Equinox.Infra.Data/Migrations/EventStoreSql/20250408031128_SQLite.Designer.cs new file mode 100644 index 00000000..79736d5d --- /dev/null +++ b/src/Equinox.Infra.Data/Migrations/EventStoreSql/20250408031128_SQLite.Designer.cs @@ -0,0 +1,53 @@ +// +using System; +using Equinox.Infra.Data.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Equinox.Infra.Data.Migrations.EventStoreSql +{ + [DbContext(typeof(EventStoreSqlContext))] + [Migration("20250408031128_SQLite")] + partial class SQLite + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Equinox.Domain.Core.Events.StoredEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AggregateId") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("MessageType") + .HasColumnType("varchar(100)") + .HasColumnName("Action"); + + b.Property("Timestamp") + .HasColumnType("TEXT") + .HasColumnName("CreationDate"); + + b.Property("User") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("StoredEvent"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Equinox.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.cs b/src/Equinox.Infra.Data/Migrations/EventStoreSql/20250408031128_SQLite.cs similarity index 55% rename from src/Equinox.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.cs rename to src/Equinox.Infra.Data/Migrations/EventStoreSql/20250408031128_SQLite.cs index 843e8a4b..176b3cf8 100644 --- a/src/Equinox.Infra.Data/Migrations/EventStoreSQL/20161213130520_Initial.cs +++ b/src/Equinox.Infra.Data/Migrations/EventStoreSql/20250408031128_SQLite.cs @@ -1,22 +1,26 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace Equinox.Infra.Data.Migrations.EventStoreSQL +#nullable disable + +namespace Equinox.Infra.Data.Migrations.EventStoreSql { - public partial class Initial : Migration + /// + public partial class SQLite : Migration { + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "StoredEvent", columns: table => new { - Id = table.Column(nullable: false), - AggregateId = table.Column(nullable: false), - Data = table.Column(nullable: true), + Id = table.Column(type: "TEXT", nullable: false), + Data = table.Column(type: "TEXT", nullable: true), + User = table.Column(type: "TEXT", nullable: true), Action = table.Column(type: "varchar(100)", nullable: true), - CreationDate = table.Column(nullable: false), - User = table.Column(nullable: true) + AggregateId = table.Column(type: "TEXT", nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false) }, constraints: table => { @@ -24,6 +28,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }); } + /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( diff --git a/src/Equinox.Services.Api/Equinox.Services.Api.csproj b/src/Equinox.Services.Api/Equinox.Services.Api.csproj index 4296d3e5..effa3002 100644 --- a/src/Equinox.Services.Api/Equinox.Services.Api.csproj +++ b/src/Equinox.Services.Api/Equinox.Services.Api.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 disable b543be42-f7ab-48b6-b633-72d6fb529fb7 enable @@ -8,15 +8,15 @@ ..\.. - - - - - - - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs b/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs index 2b8cf404..063bd329 100644 --- a/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs +++ b/src/Equinox.UI.Web/Configurations/DatabaseConfig.cs @@ -9,13 +9,36 @@ public static WebApplicationBuilder AddDatabaseConfiguration(this WebApplication { if (builder == null) throw new ArgumentNullException(nameof(builder)); + if (builder.Environment.IsDevelopment()) + { + + builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); + + builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); + + return builder; + } + builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + return builder; } + + + public static WebApplication UseDbSeed(this WebApplication app) + { + ArgumentNullException.ThrowIfNull(app); + + DbMigrationHelpers.EnsureSeedData(app).Wait(); + + return app; + } } } \ No newline at end of file diff --git a/src/Equinox.UI.Web/Configurations/DbMigrationHelpers.cs b/src/Equinox.UI.Web/Configurations/DbMigrationHelpers.cs new file mode 100644 index 00000000..e5ab8dd9 --- /dev/null +++ b/src/Equinox.UI.Web/Configurations/DbMigrationHelpers.cs @@ -0,0 +1,99 @@ +using Equinox.Domain.Core.Events; +using Equinox.Domain.Events; +using Equinox.Domain.Models; +using Equinox.Infra.CrossCutting.Identity.Data; +using Equinox.Infra.Data.Context; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using NetDevPack.Messaging; +using Newtonsoft.Json; + +namespace Equinox.UI.Web.Configurations +{ + public static class DbMigrationHelpers + { + public static async Task EnsureSeedData(WebApplication serviceScope) + { + var services = serviceScope.Services.CreateScope().ServiceProvider; + await EnsureSeedData(services); + } + + public static async Task EnsureSeedData(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.GetRequiredService().CreateScope(); + var env = scope.ServiceProvider.GetRequiredService(); + + var context = scope.ServiceProvider.GetRequiredService(); + var contextStore = scope.ServiceProvider.GetRequiredService(); + var contextIdentity = scope.ServiceProvider.GetRequiredService(); + + if (env.IsDevelopment() || env.IsEnvironment("Docker")) + { + await context.Database.MigrateAsync(); + await contextStore.Database.MigrateAsync(); + await contextIdentity.Database.MigrateAsync(); + + await EnsureSeedProducts(context, contextStore, contextIdentity); + } + } + + private static async Task EnsureSeedProducts(EquinoxContext context, + EventStoreSqlContext contextStore, + EquinoxIdentityContext contextIdentity) + { + if (contextIdentity.Users.Any()) + return; + + var userId = Guid.NewGuid(); + + await contextIdentity.Users.AddAsync(new IdentityUser + { + Id = userId.ToString(), + UserName = "teste@teste.com", + NormalizedUserName = "TESTE@TESTE.COM", + Email = "teste@teste.com", + NormalizedEmail = "TESTE@TESTE.COM", + AccessFailedCount = 0, + LockoutEnabled = false, + PasswordHash = "AQAAAAIAAYagAAAAEEdWhqiCwW/jZz0hEM7aNjok7IxniahnxKxxO5zsx2TvWs4ht1FUDnYofR8JKsA5UA==", + TwoFactorEnabled = false, + ConcurrencyStamp = Guid.NewGuid().ToString(), + EmailConfirmed = true, + SecurityStamp = Guid.NewGuid().ToString() + }); + + await contextIdentity.UserClaims.AddAsync(new IdentityUserClaim + { + UserId = userId.ToString(), + ClaimType = "Customers", + ClaimValue = "Write,Remove" + }); + + await contextIdentity.SaveChangesAsync(); + + if (context.Customers.Any()) + return; + + var customer = new Customer( + Guid.NewGuid(), + "Eduardo Pires", + "contato@eduardopires.net.br", + new DateTime(1982, 04, 24)); + + await context.Customers.AddAsync(customer); + + await context.SaveChangesAsync(); + + var customerEvent = new CustomerRegisteredEvent(customer.Id, + customer.Name, + customer.Email, + customer.BirthDate); + + var serializedData = JsonConvert.SerializeObject(customerEvent); + + await contextStore.StoredEvent.AddAsync(new StoredEvent(customerEvent, serializedData, "teste@teste.com")); + + await contextStore.SaveChangesAsync(); + } + } +} diff --git a/src/Equinox.UI.Web/Configurations/IdentityConfig.cs b/src/Equinox.UI.Web/Configurations/IdentityConfig.cs deleted file mode 100644 index 9780c237..00000000 --- a/src/Equinox.UI.Web/Configurations/IdentityConfig.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Equinox.UI.Web.Configurations -{ - public static class IdentityConfig - { - - - } -} \ No newline at end of file diff --git a/src/Equinox.UI.Web/Equinox.UI.Web.csproj b/src/Equinox.UI.Web/Equinox.UI.Web.csproj index cb732910..b336a29d 100644 --- a/src/Equinox.UI.Web/Equinox.UI.Web.csproj +++ b/src/Equinox.UI.Web/Equinox.UI.Web.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 disable aspnet-Equinox.UI.Web-61A38C1A-B3EE-4175-AD27-CD2A22786741 enable @@ -8,19 +8,19 @@ ..\.. - - - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/Equinox.UI.Web/EquinoxProject.db b/src/Equinox.UI.Web/EquinoxProject.db new file mode 100644 index 0000000000000000000000000000000000000000..0de02ecf623141161c863ee065d9f7dd83cbe849 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYDWB?0`6a1SP1wjP_-RDi_;nb w{ZrR23J7>AFym=&Y9nAa3jzY(3e0)ioZ<+~Lx6y{0t?3h3cX;)rZz9b%G=d1DgwEy0|_jzwu^4s6;3*PEjwt0WL+h*Hlqp$y4SUkA@&<~Ef z-~0aUotNM9Q!>kLod5OZ@TX5LH~r7V;gPf=GSgB%D=3VQY+YApyPjdlx^;zrbTj&* z+c&81v|nm^hH0y}b+(?W5lOrQccq}>*kA=v5#xp*gu-bk+78wo2jxodhvHgXl;_0j?D?)b8 z!^HWcB&kAj?JZpdUHv;>?=N`nBlf%6Z~W>J`Zs%_D`C^s3o;A=2tWV=5P$##AOHaf zKmY;|fWW#1O#K3Pd}H`Cx4h6Xs$I)}(&l<{-RWRO2tWV=5P$##AOHafKmY;|fB*z) zM&N3Dy=|c)}@%cEPFEG$erdXC_*K{AjuRk&LC-v#S+Fj*7 z0^@xHv|qtEz5e?MTz*@9M+do&prfPX7c~Qij6eVa5P$##AOHafKmY;|coPdO9j$Y^ zgF*Z9m_=m#4QW}C@{8u!^}@{hwc_Io>y$#|i_!KpR^AlZzILE|F(Hds`SHAznolZ6 z#Jo(x_{)+d0rQc1hx>TYZcFD<;(~l4L++kW2=j`hPL~Db>m}tZ%S!(SZlaB3i`++0 zPs^g;M{uvd^SvLvru7i+BcLjHK>z{}fB*y_009U<00Izz00g$Y0PW{!(E9~0{I7WB z9bb+;(n5=^J*sfa1E7EqfB*y_009U<00Izz00bZafj6;$={|zS`^t7T zU_LxgLgcd~1ikpC(Ht0GoL>rtuNnhy*v2C6Jt|~H4|7t;CufCxC&#i~#mB_!kCErI zLON4rorCGTGCL?JBrQ`wK8c*?8XazaZ>7ljV(t=L@vQ^*5vXGcyov84c%tR%-#-4^ z2R@-ackmgT>zR!`5Rp0rAOHafKmY;|fB*y_009U<00J8*u*Y5>bkoNVvVOLo^>G~Q zPiVma13heykL+2!*00}3F#cz~i6`3c{5rjl!0vj^Mt<;u00bZa0SG_<0uX=z1Rwwb z2tZ(S3+%2BHd}TMkYY8`qUrqt(_j3%GdEAZ@C`h7aC28Uat{FrKmY;|fB*y_009U< z00IzLrvQCcfJ5&Wm^gRfzF&Oy$X$5u;5zlN8U!E!0SG_<0uX=z1Rwwb2tZ(S3)GS9 z7yAVs`cCure*c2+pdK&adUA6Y4!MT_1Rwwb2tWV=5P$##AOHafyg3D~w%6On=)D78 z*6(He8PT&k2c?Dp z1Rwwb2tWV=5P$##AOL~QE>N*wz&{Y^_4!Ku0;X{ae?0&D_jQ>y3R@p&yI@EeZy^8y2tWV=5P$##AOHcQ!16AK)4gw>eMMHs)tgYHyqMxo ziaEvb)@dGF&#>eoM%OcKWih@U852*?$Hbc)8PUhoo0P@8#q3a$4*XU=)}CRz(9BF; zoFU`vts3@EPdS_y1)7NUV0;0m!yVaIX)r!RVG^8Sq6y;^76&K9IECUuB9Y*SM$ed>k&`ArXk4Mn((D^@l@!Xx?(w{q6`-MghApijgKmY;|fB*y_ z009U<00Izr9Rl=O0Zn?pz~{dCq>j@d!3gMIxsVfB*y_009U<00Izz00bZafs(+w$0IQH z3-o{VC+@dA`1TiQzkr=|3ZP%0B#L(sfB*y_009U<00Izz00bZafz2aOv0tDs*xwrr z_`6y50Lzl{n8qo5?(}5 zVoYXeRWgNS)dD8;+zgGy_{gCsGt3_&DeLJmF@A`T@zF3p!FaUXsGE_ZdPo_%n6W4` z$dB-Go(YF0!l6O_Vsb$9FDG;?a}Ek~yDV~`ZdN%5?JZr`?r^vdcUKxtrc5=#mCe|> z!n&Mpuh)M2&ayT!BF&_8hDM=HZ>!WQw8f=np-zwIrIYDYX_%d{I1@FDq@yn$78mt_ zcq*(hn3m@jihEv+c5Am^L zH82$`K;PO}0intGSR_jJH_Auj`fy5XRj)@)CB8Uk)h^U!M}*A0#X@7YOs5SkTN_=6 zXuXo|qSNNwI9Q!^B^Pn?2Fs(qP6Ph;N zZc5c~zS9=JmP#${%)?Zz)WTVpu{vho*h(#BYePQ_$f@{jrbQj{?8AG@jw{0!Jr9O6ZE535YpCzp6d^ljv7GFItdJ*%OT|^jXQiAN zozG5{K7R})F)Uq2ZsVt#K%U<;yWghF0o<&#BsWYFkRb<<4IQWOvE@&_``<=C zb~SzOV7==hb(})-A6^iE00bZa0SG_<0uX=z1Rwwb2-Jpvc{D;pH$lC53CFE(oWf>W zEInS}#Qd#Y&%AQxVg0#-wNYf`0s;_#00bZa0SG_<0uX=z1Rwx`4H8g?B-Hx_KL6K8 z9{F?Thd;tNg&U-eWFY_n2tWV=5P$##AOHafKmY=@CD2NRnGCMZ7?5 zRT6Ro0SG_<0uX=z1Rwwb2tWV=5ZE9A`s~4CzrZ7(yx=_Zy-$7#@d6v9jbtGJ0SG_< z0uX=z1Rwwb2tWV=wI!g22I%nu&m1{*>EGV>!{-q%P+OIR+&};V5P$##AOHafKmY;| zfB*zGNT7qJtM?1Ee70fYv0Kl52>k*Zq>W@D009U<00Izz00bZa0SG_<0<|UJqS?{= z1=5{Qf4=$S?0NJH)K(=SHxPgT1Rwwb2tWV=5P$##AOL|464*x5)%yiT?tT0xPyMVJahr8y9)a-CNs1unL@H^k_kOGLt`;Mawu9zSx=9N@k4x!kB0dPy}0UTq^KTJhAw6- z$_(-&JTVD}Cc>dX{$g@KndF3yWzIoiZkI(4)XggApuMHbW@~6V++DqOXat7Fq0ZR3 z!n&Mpuh)M2&a!qlBF&_8hDM=HZ>!WQw8f=np-zwIrIYEDn6J<}^u@#CVwE)p)AC$K zSd0o;QD4*0%5+t$CTMIPrdFnB*xby_*(R;cn8&J3PD|6!lsBlx>vpZ@d{D@Zp~|&6 z+gEHIYMp<;S61iYjF8S6>Rg+4RjPAsajDMfkxmY0{tzE4o(Y(W6`*fztbovDd@K?r z`y1t>as4E+R`q(+RN{+sR_#Jvc0|a`TP!qY%XHe%vbE85INt7{XZvGn1r*NdvUp-% z%q7LL$qo~L*}~2oIVARzExT51ZL2Gl9^+bpwDWgqy4{p&;q+cw{8}p2xOLS^r7KtZ zWO!pMwUn(5{V*V>;kIhvum5(CX^g(GnWGb92 zqf$OAWYVX_6y0fsmH4cX&RCpjOB+{OL!BR%a?|O2R!yw6Zv%2-!J{5h;1IG_UZv;#VTWPKyhxBoa!Jvw%EA>Of3|rTLt)`n*b-TcFk9j_j-c$#42f zD7P&gZ&F+7irvsk=|NIxrRv*et5&KmF`pSsAGy}8w6K-ojG(uH3XQMS$SSO`M&(nM z=IShJp7d|Z>O{-(E{Db z4cF*!^Lr~DfyLYeCP@#ob`N9UA+U#588M-E0PmN=`*Ty+>k%G$zR^3eu0NB|KNRhJ8rxa z_YqJPydVGp2tWV=5P$##AOHafKmY<;UcgODsK*OD{=}Dk&ByYu(0Bn?!*6V^FLiXc zEwlw&rdswjEjNXnfA1V~{G;Pg`VL9SSI~~WP}Df zl>(0Bs|sDalhMfGNuGol4Dv^{z$4nludeN((K&`7C30D`(^I;Tq^IoZdhg{<_glyw zo7Fw4{r}~=+f&$GVLzpHs_aC&n9z0RGN=2xZu`<6bq|rFhEM|SvJ?FsNtMc{&b`vS z(~6sw=Rw_=7FJ8u)3s#Z?R1B{_NBM1-hUw?glZTwpv}HwjU1RZEYE_rkmf;4nC8J< zF%O0Kit;>YvoBvG52g*v^Pnv><{`67Yg$*R3{04dBG<$c25|`E}hOL!acxj=r=3jT5;L24ywGa|Xz3NhGp;}~N z`Sc})tgng?)K({1TTh6f+IsXTEB*Rr*=0KIN}lO@lYR|(9pOEuurEDthU;6UhD+kT z%-JC`wE8XVu42?sA@;IFv>}mS*Y0q*Ur($b;y=}k;O6K{?YJcIotrcM^SW*8iw-WF z!Y!f!5dmrpG+uQA(9caYO0`BJKoP*nm|~5#L;xrGLcg1Ssn`1jp4#@GPi;T0^dMef zOZ=&WLO=il5P$##AOHafKmY;|fB*!l30y{3(EA0(em*VlJAKDz&@WJp4a*?_0SG_< z0uX=z1Rwwb2tWV=TSDM+S_-{i;EK`sEnm6oxCi|LTcU1J2nav`0uX=z1Rwwb2tWV= z5P(26fh*_=dcVMh-dp5-k zet~LiSPlUQKmY;|fB*y_009U<00I!$5&~C|Qe36?3(O5X{JY`%-}5%~3v7wHMIj&n z0SG_<0uX=z1Rwwb2tWV=)dcFu3dMebzr1pH*Nf+(-)*N`ZL`^2PgdI;%OL;(2tWV= z5P$##AOHafKmY;|s6BzJ?e(^?UbZ*jW&K{ZpJDw6SoVOwzo#$Q-y010yU7&Gl6<$) zyzB7-UwP#VM{oN`>j!DSfW6~I8~MQt0uX=z1Rwwb2tWV=5P$##Ah2ZwmfP*yC%eh3 zc_}bCX=`dCKYljp8wgHMdsCc0;Poe`IPZY3x7XXpP5Y<&1a7J?*{_JQB3`fl?nz47 zI6o2RuUCKfgvUnLUK%xwvWX6emo31#zmEEb8y)rUIm5 z+i2zL{Q}VgZD-Hi@t#-ceFS#bzuU+UUJ!r)1Rwwb2tWV=5P$##AOHafY%+ms>L;7m z)89dg)j^A<_Y2(k>^~p>jNjL(#tXQ9X(K;)K>z{}fB*y_009U<00Izz00bbg`2}{` z>%Gk(c`hm{ld_l(XM}WCCdG2mqUrqtpMG@dxo`h#{+(LCzy+J@0$qw11Rwwb2tWV= z5P$##AOHafKmY=@DDX~uqiu5UTh|jB5T2J6DJ$mXoAPNz+#eIO(n*n&>2g{wyb z^{wn9FCTvO5%dexcwHiU5P$##AOHafKmY;|fB*y_0D+4Yuita{5feJ^>~3x`(l~zo%8%kjTh*cu#q3UAOHafKmY;|fB*y_ z009U<00I{yu<|y0)8yIfy5Bw-4hIKA!Gi-KZ#Wbh@cM^BgWeG98}#~pey*4E1$?1i z{uWOpUHsHacW?iUKjW5_e6!PNY*x8At$r>tdP!-`DuARsAYv*K*cseV}ijbY7Ibz8LdUAE0?T@n@y}Irn=-~$X0s{fg%lgO+ zvi_(j%fgHpU!0?9>QDTSi8E
    -@**@ - -