Repository: EduardoPires/EquinoxProject
Branch: master
Commit: fde9a953eb31
Files: 179
Total size: 1.3 MB
Directory structure:
gitextract_ug6sjdzw/
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── custom.md
│ │ └── feature_request.md
│ └── workflows/
│ └── dotnet-core.yml
├── .gitignore
├── AGENTS.md
├── Equinox.sln
├── LICENSE
├── README.md
├── docs/
│ ├── Architecture.dgml
│ ├── _config.yml
│ └── index.md
├── sql/
│ └── GenerateDataBase.sql
├── src/
│ ├── Equinox.Application/
│ │ ├── Equinox.Application.csproj
│ │ ├── EventSourcedNormalizers/
│ │ │ ├── CustomerHistory.cs
│ │ │ └── CustomerHistoryData.cs
│ │ ├── Extensions/
│ │ │ └── CustomerExtensions.cs
│ │ ├── Interfaces/
│ │ │ └── ICustomerAppService.cs
│ │ ├── Services/
│ │ │ └── CustomerAppService.cs
│ │ └── ViewModels/
│ │ └── CustomerViewModel.cs
│ ├── Equinox.Domain/
│ │ ├── Commands/
│ │ │ ├── CustomerCommand.cs
│ │ │ ├── CustomerCommandHandler.cs
│ │ │ ├── RegisterNewCustomerCommand.cs
│ │ │ ├── RemoveCustomerCommand.cs
│ │ │ ├── UpdateCustomerCommand.cs
│ │ │ └── Validations/
│ │ │ ├── CustomerValidation.cs
│ │ │ ├── RegisterNewCustomerCommandValidation.cs
│ │ │ ├── RemoveCustomerCommandValidation.cs
│ │ │ └── UpdateCustomerCommandValidation.cs
│ │ ├── Equinox.Domain.csproj
│ │ ├── Events/
│ │ │ ├── CustomerEventHandler.cs
│ │ │ ├── CustomerRegisteredEvent.cs
│ │ │ ├── CustomerRemovedEvent.cs
│ │ │ └── CustomerUpdatedEvent.cs
│ │ ├── Interfaces/
│ │ │ └── ICustomerRepository.cs
│ │ └── Models/
│ │ └── Customer.cs
│ ├── Equinox.Domain.Core/
│ │ ├── Equinox.Domain.Core.csproj
│ │ └── Events/
│ │ ├── IEventStore.cs
│ │ └── StoredEvent.cs
│ ├── Equinox.Infra.CrossCutting.Bus/
│ │ ├── Equinox.Infra.CrossCutting.Bus.csproj
│ │ └── InMemoryBus.cs
│ ├── Equinox.Infra.CrossCutting.Identity/
│ │ ├── API/
│ │ │ ├── AppJwtSettings.cs
│ │ │ └── JwtBuilder.cs
│ │ ├── Authorization/
│ │ │ ├── CustomAuthorizationValidation.cs
│ │ │ ├── CustomAuthorizeAttribute.cs
│ │ │ └── RequerimentClaimFilter.cs
│ │ ├── Configuration/
│ │ │ └── AspNetIdentityConfig.cs
│ │ ├── Data/
│ │ │ └── EquinoxIdentityContext.cs
│ │ ├── Equinox.Infra.CrossCutting.Identity.csproj
│ │ ├── Extensions/
│ │ │ └── ClaimsPrincipalExtensions.cs
│ │ ├── Migrations/
│ │ │ ├── 20250408033115_SQLite.Designer.cs
│ │ │ ├── 20250408033115_SQLite.cs
│ │ │ └── EquinoxIdentityContextModelSnapshot.cs
│ │ ├── Models/
│ │ │ ├── LoginUser.cs
│ │ │ ├── RegisterUser.cs
│ │ │ ├── UserClaim.cs
│ │ │ ├── UserResponse.cs
│ │ │ └── UserToken.cs
│ │ └── User/
│ │ ├── AspNetUser.cs
│ │ └── IAspNetUser.cs
│ ├── Equinox.Infra.CrossCutting.IoC/
│ │ ├── Equinox.Infra.CrossCutting.IoC.csproj
│ │ └── NativeInjectorBootStrapper.cs
│ ├── Equinox.Infra.Data/
│ │ ├── Context/
│ │ │ ├── EquinoxContext.cs
│ │ │ └── EventStoreSQLContext.cs
│ │ ├── Equinox.Infra.Data.csproj
│ │ ├── EventSourcing/
│ │ │ └── SqlEventStore.cs
│ │ ├── Mappings/
│ │ │ ├── CustomerMap.cs
│ │ │ └── StoredEventMap.cs
│ │ ├── Migrations/
│ │ │ ├── 20250408031104_SQLite.Designer.cs
│ │ │ ├── 20250408031104_SQLite.cs
│ │ │ ├── EquinoxContextModelSnapshot.cs
│ │ │ ├── EventStoreSQL/
│ │ │ │ └── EventStoreSQLContextModelSnapshot.cs
│ │ │ └── EventStoreSql/
│ │ │ ├── 20250408031128_SQLite.Designer.cs
│ │ │ └── 20250408031128_SQLite.cs
│ │ ├── Repository/
│ │ │ ├── CustomerRepository.cs
│ │ │ └── EventSourcing/
│ │ │ ├── EventStoreSQLRepository.cs
│ │ │ └── IEventStoreRepository.cs
│ │ └── appsettings.json
│ ├── Equinox.Services.Api/
│ │ ├── Configurations/
│ │ │ ├── ApiConfig.cs
│ │ │ ├── DatabaseConfig.cs
│ │ │ ├── DependencyInjectionConfig.cs
│ │ │ └── SwaggerConfig.cs
│ │ ├── Controllers/
│ │ │ ├── AccountController.cs
│ │ │ ├── ApiController.cs
│ │ │ └── CustomerController.cs
│ │ ├── Dockerfile
│ │ ├── Equinox.Services.Api.csproj
│ │ ├── Program.cs
│ │ ├── Properties/
│ │ │ └── launchSettings.json
│ │ ├── appsettings.Development.json
│ │ ├── appsettings.Staging.json
│ │ ├── appsettings.Testing.json
│ │ └── appsettings.json
│ └── Equinox.UI.Web/
│ ├── Areas/
│ │ └── Identity/
│ │ ├── IdentityHostingStartup.cs
│ │ └── Pages/
│ │ ├── Account/
│ │ │ ├── Login.cshtml
│ │ │ ├── Login.cshtml.cs
│ │ │ ├── Logout.cshtml
│ │ │ ├── Logout.cshtml.cs
│ │ │ ├── Register.cshtml
│ │ │ ├── Register.cshtml.cs
│ │ │ └── _ViewImports.cshtml
│ │ ├── _ValidationScriptsPartial.cshtml
│ │ ├── _ViewImports.cshtml
│ │ └── _ViewStart.cshtml
│ ├── Configurations/
│ │ ├── DatabaseConfig.cs
│ │ ├── DbMigrationHelpers.cs
│ │ ├── DependencyInjectionConfig.cs
│ │ └── MvcConfig.cs
│ ├── Controllers/
│ │ ├── BaseController.cs
│ │ ├── CustomerController.cs
│ │ └── HomeController.cs
│ ├── Data/
│ │ ├── ApplicationDbContext.cs
│ │ └── Migrations/
│ │ ├── 00000000000000_CreateIdentitySchema.Designer.cs
│ │ ├── 00000000000000_CreateIdentitySchema.cs
│ │ └── ApplicationDbContextModelSnapshot.cs
│ ├── Dockerfile
│ ├── Equinox.UI.Web.csproj
│ ├── Models/
│ │ └── ErrorViewModel.cs
│ ├── Program.cs
│ ├── Properties/
│ │ ├── launchSettings.json
│ │ ├── serviceDependencies.json
│ │ └── serviceDependencies.local.json
│ ├── ScaffoldingReadMe.txt
│ ├── ViewComponents/
│ │ └── SummaryViewComponent.cs
│ ├── Views/
│ │ ├── Customer/
│ │ │ ├── Create.cshtml
│ │ │ ├── Delete.cshtml
│ │ │ ├── Details.cshtml
│ │ │ ├── Edit.cshtml
│ │ │ └── Index.cshtml
│ │ ├── Home/
│ │ │ ├── Index.cshtml
│ │ │ └── Privacy.cshtml
│ │ ├── Shared/
│ │ │ ├── Components/
│ │ │ │ └── Summary/
│ │ │ │ └── Default.cshtml
│ │ │ ├── Error.cshtml
│ │ │ ├── _CookieConsentPartial.cshtml
│ │ │ ├── _Layout.cshtml
│ │ │ ├── _LoginPartial.cshtml
│ │ │ └── _ValidationScriptsPartial.cshtml
│ │ ├── _ViewImports.cshtml
│ │ └── _ViewStart.cshtml
│ ├── appsettings.Development.json
│ ├── appsettings.Staging.json
│ ├── appsettings.Testing.json
│ ├── appsettings.json
│ ├── bundleconfig.json
│ └── wwwroot/
│ ├── _references.js
│ ├── css/
│ │ └── site.css
│ ├── js/
│ │ └── site.js
│ └── lib/
│ ├── bootstrap/
│ │ ├── .bower.json
│ │ ├── LICENSE
│ │ └── dist/
│ │ ├── css/
│ │ │ ├── bootstrap-grid.css
│ │ │ ├── bootstrap-reboot.css
│ │ │ ├── bootstrap-theme.css
│ │ │ └── bootstrap.css
│ │ └── js/
│ │ ├── bootstrap.bundle.js
│ │ ├── bootstrap.js
│ │ └── npm.js
│ ├── jquery/
│ │ ├── .bower.json
│ │ ├── LICENSE.txt
│ │ └── dist/
│ │ └── jquery.js
│ ├── jquery-validation/
│ │ ├── .bower.json
│ │ ├── LICENSE.md
│ │ └── dist/
│ │ ├── additional-methods.js
│ │ └── jquery.validate.js
│ ├── jquery-validation-unobtrusive/
│ │ ├── .bower.json
│ │ ├── LICENSE.txt
│ │ └── jquery.validate.unobtrusive.js
│ └── qrcode.js
└── tests/
└── Equinox.Tests.Architecture/
├── DataBaseTests.cs
├── DomainTests.cs
├── Equinox.Tests.Architecture.csproj
├── GeneralPatternTests.cs
├── Support/
│ ├── ShouldUseDependencyInjectionRule.cs
│ ├── TestOutputHelperTextWriter.cs
│ └── TestsSupport.cs
└── WebApplicationTests.cs
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
================================================
FILE: .editorconfig
================================================
# Remove unused using directives
dotnet_diagnostic.CS8019.severity = error
# Unused variables
dotnet_diagnostic.IDE0059.severity = error
# Unused expressions (e.g., method call with no effect)
dotnet_diagnostic.IDE0058.severity = error
# Parameter is never used
dotnet_diagnostic.IDE0060.severity = warning
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
patreon: eduardopires
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/custom.md
================================================
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
**ONLY IN ENGLISH**
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/dotnet-core.yml
================================================
name: .NET Core
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
[Xx]64/
[Xx]86/
[Bb]uild/
bld/
[Bb]in/
[Oo]bj/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Un-comment the next line if you do not want to checkin
# your web deploy settings because they may include unencrypted
# passwords
#*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directory
AppPackages/
BundleArtifacts/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# LightSwitch generated files
GeneratedArtifacts/
ModelManifest.xml
# Paket dependency manager
.paket/paket.exe
# FAKE - F# Make
.fake/
================================================
FILE: AGENTS.md
================================================
# AGENTS Setup Guide
This repository requires specific environment configuration to build and run .NET 9.0 projects. **All dependencies must be installed during the setup script phase,** especially in restricted environments (e.g., OpenAI Codex).
## Setup Steps (`setup.sh`)
1. **Update and upgrade the system:**
`apt-get update && apt-get upgrade -y`
2. **Install essential dependencies (`wget`, `apt-transport-https`, `ca-certificates`):**
`apt-get install -y wget apt-transport-https ca-certificates`
3. **Configure Microsoft package source and update package lists:**
`wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb`
`dpkg -i packages-microsoft-prod.deb`
`rm packages-microsoft-prod.deb`
`apt-get update`
4. **Install .NET SDK 9.0:**
`apt-get install -y dotnet-sdk-9.0`
5. **Verify installation:**
`dotnet --version`
6. **(Optional) Remove obsolete packages:**
`apt-get autoremove -y`
---
## ⚠️ Notes for Codex and Similar Cloud Environments
- **Internet access is only available during the setup script phase.** All dependencies must be installed in this initial script.
- **A network proxy is always active:** Standard environment variables like `http_proxy`, `https_proxy`, and the proxy certificate (`$CODEX_PROXY_CERT`) are automatically set. These should be respected by all package managers and CLI tools.
- **If you encounter connectivity issues:**
- Ensure your setup script is running during the setup/initialization phase (not in the regular terminal).
- Check that all required dependencies are installed up front.
- Make sure environment variables for the proxy/certificate are being used by your tools.
---
## General Recommendations
- Use the latest version of **Visual Studio** or **Visual Studio Code** for development.
- Always install the correct SDK and runtime versions as required by this project.
- The project can be built and run in Visual Studio Code on Windows, Linux, or macOS.
---
**IMPORTANT:**
If using a cloud development platform or automated environment, ensure all tools and dependencies are installed during the setup script phase. No additional internet access will be available after environment initialization. Plan accordingly.
================================================
FILE: Equinox.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# 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
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2 - Services", "2 - Services", "{A5DF9A6E-87DC-49E9-A437-04C399572BD4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 - Application", "3 - Application", "{FD1B5301-2A07-47B3-8773-839429800B2F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4 - Domain", "4 - Domain", "{0C6FAB88-2741-4295-B6B6-F397DBA958F0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 - Infra", "5 - Infra", "{BAC12BD1-13B1-416E-A4B8-D889C6FFACCC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.1 - Data", "5.1 - Data", "{0CA11832-7E0F-4038-9DB6-FF1E6D14D0DF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.2 - CrossCutting", "5.2 - CrossCutting", "{DF6C4BDE-F3C5-4E53-A5D5-9D27B2D3E38F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.UI.Web", "src\Equinox.UI.Web\Equinox.UI.Web.csproj", "{490517BA-F3C3-44B4-82F8-1E3A4ED6777A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Services.Api", "src\Equinox.Services.Api\Equinox.Services.Api.csproj", "{EA966EC3-85A6-4B57-82C1-5120E3390243}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Application", "src\Equinox.Application\Equinox.Application.csproj", "{851E7338-2397-4E8D-8199-FD1EAD109418}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Domain", "src\Equinox.Domain\Equinox.Domain.csproj", "{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Domain.Core", "src\Equinox.Domain.Core\Equinox.Domain.Core.csproj", "{9CC884B7-FA70-49E1-92A6-2B566E00FAB9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equinox.Infra.Data", "src\Equinox.Infra.Data\Equinox.Infra.Data.csproj", "{F0DDF87D-98A4-4237-91C9-FD865ED78ABB}"
EndProject
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("{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("{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
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{490517BA-F3C3-44B4-82F8-1E3A4ED6777A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{490517BA-F3C3-44B4-82F8-1E3A4ED6777A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{490517BA-F3C3-44B4-82F8-1E3A4ED6777A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{490517BA-F3C3-44B4-82F8-1E3A4ED6777A}.Release|Any CPU.Build.0 = Release|Any CPU
{EA966EC3-85A6-4B57-82C1-5120E3390243}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA966EC3-85A6-4B57-82C1-5120E3390243}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA966EC3-85A6-4B57-82C1-5120E3390243}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA966EC3-85A6-4B57-82C1-5120E3390243}.Release|Any CPU.Build.0 = Release|Any CPU
{851E7338-2397-4E8D-8199-FD1EAD109418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{851E7338-2397-4E8D-8199-FD1EAD109418}.Debug|Any CPU.Build.0 = Debug|Any CPU
{851E7338-2397-4E8D-8199-FD1EAD109418}.Release|Any CPU.ActiveCfg = Release|Any CPU
{851E7338-2397-4E8D-8199-FD1EAD109418}.Release|Any CPU.Build.0 = Release|Any CPU
{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA}.Release|Any CPU.Build.0 = Release|Any CPU
{9CC884B7-FA70-49E1-92A6-2B566E00FAB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9CC884B7-FA70-49E1-92A6-2B566E00FAB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9CC884B7-FA70-49E1-92A6-2B566E00FAB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9CC884B7-FA70-49E1-92A6-2B566E00FAB9}.Release|Any CPU.Build.0 = Release|Any CPU
{F0DDF87D-98A4-4237-91C9-FD865ED78ABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0DDF87D-98A4-4237-91C9-FD865ED78ABB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0DDF87D-98A4-4237-91C9-FD865ED78ABB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0DDF87D-98A4-4237-91C9-FD865ED78ABB}.Release|Any CPU.Build.0 = Release|Any CPU
{91F0D76D-2BEA-43D2-B123-DC29F2E87792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91F0D76D-2BEA-43D2-B123-DC29F2E87792}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91F0D76D-2BEA-43D2-B123-DC29F2E87792}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91F0D76D-2BEA-43D2-B123-DC29F2E87792}.Release|Any CPU.Build.0 = Release|Any CPU
{788030D0-561B-4136-96B4-D5ABFB5CFD07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{788030D0-561B-4136-96B4-D5ABFB5CFD07}.Debug|Any CPU.Build.0 = Debug|Any CPU
{788030D0-561B-4136-96B4-D5ABFB5CFD07}.Release|Any CPU.ActiveCfg = Release|Any CPU
{788030D0-561B-4136-96B4-D5ABFB5CFD07}.Release|Any CPU.Build.0 = Release|Any CPU
{B3000AD2-5EAA-49A2-8FC4-10DF329B15B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0CA11832-7E0F-4038-9DB6-FF1E6D14D0DF} = {BAC12BD1-13B1-416E-A4B8-D889C6FFACCC}
{DF6C4BDE-F3C5-4E53-A5D5-9D27B2D3E38F} = {BAC12BD1-13B1-416E-A4B8-D889C6FFACCC}
{490517BA-F3C3-44B4-82F8-1E3A4ED6777A} = {BA4C44CC-65BC-4CE0-9B44-68BA231FAC73}
{EA966EC3-85A6-4B57-82C1-5120E3390243} = {A5DF9A6E-87DC-49E9-A437-04C399572BD4}
{851E7338-2397-4E8D-8199-FD1EAD109418} = {FD1B5301-2A07-47B3-8773-839429800B2F}
{BF28C988-9C1B-41F5-BD58-D2FCAD7E80BA} = {0C6FAB88-2741-4295-B6B6-F397DBA958F0}
{9CC884B7-FA70-49E1-92A6-2B566E00FAB9} = {0C6FAB88-2741-4295-B6B6-F397DBA958F0}
{F0DDF87D-98A4-4237-91C9-FD865ED78ABB} = {0CA11832-7E0F-4038-9DB6-FF1E6D14D0DF}
{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}
EndGlobalSection
EndGlobal
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2016 Eduardo Pires
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
What is the Equinox Project?
=====================
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
[](LICENSE)
[](https://huboard.com/EduardoPires/EquinoxProject/)
## Give a Star! :star:
If you liked the project or if Equinox helped you, please give a star ;)
## Want to learn everything? :mortar_board:
Check my online courses at [desenvolvedor.io](https://desenvolvedor.io)
## How to use:
- You will need the latest version of Visual Studio and the latest .NET Core SDK.
- ***Please check if you have installed the runtime version***
- The latest SDK and tools can be downloaded from https://dot.net/core.
Also you can run the Equinox Project in Visual Studio Code (Windows, Linux or MacOS).
To know more about how to setup your enviroment visit the [Microsoft .NET Download Guide](https://www.microsoft.com/net/download)
## Technologies implemented:
- ASP.NET 9.0
- ASP.NET MVC Core
- ASP.NET WebApi Core with JWT Bearer Authentication
- ASP.NET Identity Core
- Entity Framework Core 9.0
- Custom Automatic Mapping (no more AutoMapper)
- FluentValidator
- NetDevPack.SimpleMediator (no more MediatR)
- NetDevPack (DDD, CQRS, UOW and more)
- Swagger UI with JWT support
## Architecture:
- Full architecture with responsibility separation concerns, SOLID and Clean Code
- Domain Driven Design (Layers and Domain Model Pattern)
- Domain Events
- Domain Notification
- Domain Validations
- CQRS (Imediate Consistency)
- Event Sourcing
- Unit of Work
- Repository
## News
**v1.10 - 04/08/2025**
- Migrated to .NET 9.0
- Replaced MediatR with [NetDevPack.SimpleMediator](https://github.com/NetDevPack/SimpleMediator) for lighter and native CQRS handling
- Removed AutoMapper in favor of lightweight custom mapping extensions
- Architecture Tests with [NetArchTest.Rules](https://github.com/BenMorris/NetArchTest)
- Added built-in SQLite support with automatic EF Core migrations (just run and go — no setup required)
- Updated all dependencies to the latest stable versions
**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
**v1.7 - 04/06/2021**
- Migrated for .NET 5.0
- All dependencies is up to date
**v1.6 - 06/09/2020**
- Full Refactoring (consistency, events, validation, identity)
- Added [NetDevPack](https://github.com/NetDevPack) and saving a hundreds of code lines
- All dependencies is up to date
**v1.5 - 01/22/2020**
- Migrated for .NET Core 3.1.1
- All dependencies is up to date
- Added JWT (Bearer) authentication for WebAPI
- Added JWT support in Swagger
**v1.4 - 02/14/2019**
- Migrated for .NET Core 2.2.1
- All dependencies is up to date
- Improvements for last version of MediatR (Notifications and Request)
**v1.3 - 05/22/2018**
- Migrated for .NET Core 2.1.2
- All dependencies is up to date
- Improvements in Automapper Setup
- Improvements for last version of MediatR (Notifications and Request)
- Code improvements in general
**v1.2 - 08/15/2017**
- Migrated for .NET Core 2.0 and ASP.NET Core 2.0
- Adaptations for the new Identity Authentication Model
**v1.1 - 08/09/2017**
- Adding WebAPI service exposing the application features
- Adding Swagger UI for better viewing and testing
- Adding MediatR for Memory Bus Messaging
## Disclaimer:
- **NOT** intended to be a definitive solution
- Beware to use in production way
- Maybe you don't need a lot of implementations that is included, try avoid the **over engineering**
## Pull-Requests
Make a contact! Don't submit PRs for extra features, all the new features are planned
## Why Equinox?
The Equinox is an astronomical event in which the plane of Earth's equator passes through the center of the Sun, which occurs twice each year, around 20 March and 23 September. [Wikipedia](https://en.wikipedia.org/wiki/Equinox)
Equinox is also a series of publications (subtitle: "The Review of Scientific Illuminism") in book form that serves as the official organ of the A∴A∴, a magical order founded by Aleister Crowley :) [Wikipedia](https://en.wikipedia.org/wiki/The_Equinox)
## We are Online:
See the project running on Azure
## About:
The Equinox Project was developed by [Eduardo Pires](http://eduardopires.net.br) under the [MIT license](LICENSE).
================================================
FILE: docs/Architecture.dgml
================================================
================================================
FILE: docs/_config.yml
================================================
title: The Equinox Project
description: Full ASP.NET Core 3.1 application with DDD, CQRS and Event Sourcing
google_analytics:
show_downloads: true
theme: jekyll-theme-cayman
gems:
- jekyll-mentions
================================================
FILE: docs/index.md
================================================
What is the Equinox Project?
=====================
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
[](LICENSE)
[](https://huboard.com/EduardoPires/EquinoxProject/)
## Give a Star! :star:
If you liked the project or if Equinox helped you, please give a star ;)
## Want to learn everything? :mortar_board:
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.
- ***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.
Also you can run the Equinox Project in Visual Studio Code (Windows, Linux or MacOS).
To know more about how to setup your enviroment visit the [Microsoft .NET Download Guide](https://www.microsoft.com/net/download)
## Technologies implemented:
- ASP.NET Core 3.1 (with .NET Core 3.1)
- ASP.NET MVC Core
- ASP.NET WebApi Core with JWT Bearer Authentication
- ASP.NET Identity Core
- Entity Framework Core 3.1
- .NET Core Native DI
- AutoMapper
- FluentValidator
- MediatR
- Swagger UI with JWT support
## Architecture:
- Full architecture with responsibility separation concerns, SOLID and Clean Code
- Domain Driven Design (Layers and Domain Model Pattern)
- Domain Events
- Domain Notification
- CQRS (Imediate Consistency)
- Event Sourcing
- Unit of Work
- Repository and Generic Repository
## News
**v1.5 - 01/22/2020**
- Migrated for .NET Core 3.1.1
- All dependencies is up to date
- Added JWT (Bearer) authentication for WebAPI
- Added JWT support in Swagger
**v1.4 - 02/14/2019**
- Migrated for .NET Core 2.2.1
- All dependencies is up to date
- Improvements for last version of MediatR (Notifications and Request)
**v1.3 - 05/22/2018**
- Migrated for .NET Core 2.1.2
- All dependencies is up to date
- Improvements in Automapper Setup
- Improvements for last version of MediatR (Notifications and Request)
- Code improvements in general
**v1.2 - 08/15/2017**
- Migrated for .NET Core 2.0 and ASP.NET Core 2.0
- Adaptations for the new Identity Authentication Model
**v1.1 - 08/09/2017**
- Adding WebAPI service exposing the application features
- Adding Swagger UI for better viewing and testing
- Adding MediatR for Memory Bus Messaging
## Disclaimer:
- **NOT** intended to be a definitive solution
- 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 new features is coming in V2
## Why Equinox?
The Equinox is an astronomical event in which the plane of Earth's equator passes through the center of the Sun, which occurs twice each year, around 20 March and 23 September. [Wikipedia](https://en.wikipedia.org/wiki/Equinox)
Equinox is also a series of publications (subtitle: "The Review of Scientific Illuminism") in book form that serves as the official organ of the A∴A∴, a magical order founded by Aleister Crowley :) [Wikipedia](https://en.wikipedia.org/wiki/The_Equinox)
## We are Online:
See the project running on Azure
## About:
The Equinox Project was developed by [Eduardo Pires](http://eduardopires.net.br) under the [MIT license](LICENSE).
================================================
FILE: src/Equinox.Application/Equinox.Application.csproj
================================================
net9.0
disable
================================================
FILE: src/Equinox.Application/EventSourcedNormalizers/CustomerHistory.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Equinox.Domain.Core.Events;
namespace Equinox.Application.EventSourcedNormalizers
{
public static class CustomerHistory
{
public static IList HistoryData { get; set; }
public static IList ToJavaScriptCustomerHistory(IList storedEvents)
{
HistoryData = new List();
CustomerHistoryDeserializer(storedEvents);
var sorted = HistoryData.OrderBy(c => c.Timestamp);
var list = new List();
var last = new CustomerHistoryData();
foreach (var change in sorted)
{
var jsSlot = new CustomerHistoryData
{
Id = change.Id == Guid.Empty.ToString() || change.Id == last.Id
? ""
: change.Id,
Name = string.IsNullOrWhiteSpace(change.Name) || change.Name == last.Name
? ""
: change.Name,
Email = string.IsNullOrWhiteSpace(change.Email) || change.Email == last.Email
? ""
: change.Email,
BirthDate = string.IsNullOrWhiteSpace(change.BirthDate) || change.BirthDate == last.BirthDate
? ""
: change.BirthDate.Substring(0,10),
Action = string.IsNullOrWhiteSpace(change.Action) ? "" : change.Action,
Timestamp = change.Timestamp,
Who = change.Who
};
list.Add(jsSlot);
last = change;
}
return list;
}
private static void CustomerHistoryDeserializer(IEnumerable storedEvents)
{
foreach (var e in storedEvents)
{
var historyData = JsonSerializer.Deserialize(e.Data);
historyData.Timestamp = DateTime.Parse(historyData.Timestamp).ToString("yyyy'-'MM'-'dd' - 'HH':'mm':'ss");
switch (e.MessageType)
{
case "CustomerRegisteredEvent":
historyData.Action = "Registered";
historyData.Who = e.User;
break;
case "CustomerUpdatedEvent":
historyData.Action = "Updated";
historyData.Who = e.User;
break;
case "CustomerRemovedEvent":
historyData.Action = "Removed";
historyData.Who = e.User;
break;
default:
historyData.Action = "Unrecognized";
historyData.Who = e.User ?? "Anonymous";
break;
}
HistoryData.Add(historyData);
}
}
}
}
================================================
FILE: src/Equinox.Application/EventSourcedNormalizers/CustomerHistoryData.cs
================================================
namespace Equinox.Application.EventSourcedNormalizers
{
public class CustomerHistoryData
{
public string Action { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string BirthDate { get; set; }
public string Timestamp { get; set; }
public string Who { get; set; }
}
}
================================================
FILE: src/Equinox.Application/Extensions/CustomerExtensions.cs
================================================
using Equinox.Application.ViewModels;
using Equinox.Domain.Commands;
using Equinox.Domain.Models;
using System.Collections.Generic;
using System.Linq;
namespace Equinox.Application.Extensions
{
public static class CustomerExtensions
{
public static CustomerViewModel ToViewModel(this Customer customer)
{
if (customer == null) return null;
return new CustomerViewModel
{
Id = customer.Id,
Name = customer.Name,
Email = customer.Email,
BirthDate = customer.BirthDate
};
}
public static IEnumerable ToViewModel(this IEnumerable customers)
{
return customers?.Select(c => c.ToViewModel());
}
public static Customer ToEntity(this CustomerViewModel customer)
{
if (customer == null) return null;
return new Customer(customer.Id, customer.Name, customer.Email, customer.BirthDate);
}
public static RegisterNewCustomerCommand ToRegisterCommand(this CustomerViewModel customer)
{
if (customer == null) return null;
return new RegisterNewCustomerCommand(customer.Name, customer.Email, customer.BirthDate);
}
public static UpdateCustomerCommand ToUpdateCommand(this CustomerViewModel customer)
{
if (customer == null) return null;
return new UpdateCustomerCommand(customer.Id, customer.Name, customer.Email, customer.BirthDate);
}
}
}
================================================
FILE: src/Equinox.Application/Interfaces/ICustomerAppService.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Equinox.Application.EventSourcedNormalizers;
using Equinox.Application.ViewModels;
using FluentValidation.Results;
namespace Equinox.Application.Interfaces
{
public interface ICustomerAppService : IDisposable
{
Task> GetAll();
Task GetById(Guid id);
Task Register(CustomerViewModel customerViewModel);
Task Update(CustomerViewModel customerViewModel);
Task Remove(Guid id);
Task> GetAllHistory(Guid id);
}
}
================================================
FILE: src/Equinox.Application/Services/CustomerAppService.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Equinox.Application.EventSourcedNormalizers;
using Equinox.Application.Extensions;
using Equinox.Application.Interfaces;
using Equinox.Application.ViewModels;
using Equinox.Domain.Commands;
using Equinox.Domain.Interfaces;
using Equinox.Infra.Data.Repository.EventSourcing;
using FluentValidation.Results;
using NetDevPack.Mediator;
namespace Equinox.Application.Services
{
public class CustomerAppService(ICustomerRepository customerRepository,
IMediatorHandler mediator,
IEventStoreRepository eventStoreRepository) : ICustomerAppService
{
private readonly ICustomerRepository _customerRepository = customerRepository;
private readonly IEventStoreRepository _eventStoreRepository = eventStoreRepository;
private readonly IMediatorHandler _mediator = mediator;
public async Task> GetAll()
{
return (await _customerRepository.GetAll()).ToViewModel();
}
public async Task GetById(Guid id)
{
return (await _customerRepository.GetById(id)).ToViewModel();
}
public async Task Register(CustomerViewModel customerViewModel)
{
var registerCommand = customerViewModel.ToRegisterCommand();
return await _mediator.SendCommand(registerCommand);
}
public async Task Update(CustomerViewModel customerViewModel)
{
var updateCommand = customerViewModel.ToUpdateCommand();
return await _mediator.SendCommand(updateCommand);
}
public async Task Remove(Guid id)
{
var removeCommand = new RemoveCustomerCommand(id);
return await _mediator.SendCommand(removeCommand);
}
public async Task> GetAllHistory(Guid id)
{
return CustomerHistory.ToJavaScriptCustomerHistory(await _eventStoreRepository.All(id));
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
}
================================================
FILE: src/Equinox.Application/ViewModels/CustomerViewModel.cs
================================================
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Equinox.Application.ViewModels
{
public class CustomerViewModel
{
[Key]
public Guid Id { get; set; }
[Required(ErrorMessage = "The Name is Required")]
[MinLength(2)]
[MaxLength(100)]
[DisplayName("Name")]
public string Name { get; set; }
[Required(ErrorMessage = "The E-mail is Required")]
[EmailAddress]
[DisplayName("E-mail")]
public string Email { get; set; }
[Required(ErrorMessage = "The BirthDate is Required")]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
[DataType(DataType.Date, ErrorMessage = "Data em formato inválido")]
[DisplayName("Birth Date")]
public DateTime BirthDate { get; set; }
}
}
================================================
FILE: src/Equinox.Domain/Commands/CustomerCommand.cs
================================================
using System;
using NetDevPack.Messaging;
namespace Equinox.Domain.Commands
{
public abstract class CustomerCommand : Command
{
public Guid Id { get; protected set; }
public string Name { get; protected set; }
public string Email { get; protected set; }
public DateTime BirthDate { get; protected set; }
}
}
================================================
FILE: src/Equinox.Domain/Commands/CustomerCommandHandler.cs
================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Equinox.Domain.Events;
using Equinox.Domain.Interfaces;
using Equinox.Domain.Models;
using FluentValidation.Results;
using NetDevPack.Messaging;
using NetDevPack.SimpleMediator.Core.Interfaces;
namespace Equinox.Domain.Commands
{
public class CustomerCommandHandler(ICustomerRepository customerRepository) : CommandHandler,
IRequestHandler,
IRequestHandler,
IRequestHandler
{
private readonly ICustomerRepository _customerRepository = customerRepository;
public async Task Handle(RegisterNewCustomerCommand message, CancellationToken cancellationToken)
{
if (!message.IsValid()) return message.ValidationResult;
var customer = new Customer(Guid.NewGuid(), message.Name, message.Email, message.BirthDate);
if (await _customerRepository.GetByEmail(customer.Email) != null)
{
AddError("The customer e-mail has already been taken.");
return ValidationResult;
}
customer.AddDomainEvent(new CustomerRegisteredEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate));
_customerRepository.Add(customer);
return await Commit(_customerRepository.UnitOfWork);
}
public async Task Handle(UpdateCustomerCommand message, CancellationToken cancellationToken)
{
if (!message.IsValid()) return message.ValidationResult;
var customer = new Customer(message.Id, message.Name, message.Email, message.BirthDate);
var existingCustomer = await _customerRepository.GetByEmail(customer.Email);
if (existingCustomer != null && existingCustomer.Id != customer.Id)
{
if (!existingCustomer.Equals(customer))
{
AddError("The customer e-mail has already been taken.");
return ValidationResult;
}
}
customer.AddDomainEvent(new CustomerUpdatedEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate));
_customerRepository.Update(customer);
return await Commit(_customerRepository.UnitOfWork);
}
public async Task Handle(RemoveCustomerCommand message, CancellationToken cancellationToken)
{
if (!message.IsValid()) return message.ValidationResult;
var customer = await _customerRepository.GetById(message.Id);
if (customer is null)
{
AddError("The customer doesn't exists.");
return ValidationResult;
}
customer.AddDomainEvent(new CustomerRemovedEvent(message.Id));
_customerRepository.Remove(customer);
return await Commit(_customerRepository.UnitOfWork);
}
public void Dispose()
{
_customerRepository.Dispose();
}
}
}
================================================
FILE: src/Equinox.Domain/Commands/RegisterNewCustomerCommand.cs
================================================
using System;
using Equinox.Domain.Commands.Validations;
namespace Equinox.Domain.Commands
{
public class RegisterNewCustomerCommand : CustomerCommand
{
public RegisterNewCustomerCommand(string name, string email, DateTime birthDate)
{
Name = name;
Email = email;
BirthDate = birthDate;
}
public override bool IsValid()
{
ValidationResult = new RegisterNewCustomerCommandValidation().Validate(this);
return ValidationResult.IsValid;
}
}
}
================================================
FILE: src/Equinox.Domain/Commands/RemoveCustomerCommand.cs
================================================
using System;
using Equinox.Domain.Commands.Validations;
namespace Equinox.Domain.Commands
{
public class RemoveCustomerCommand : CustomerCommand
{
public RemoveCustomerCommand(Guid id)
{
Id = id;
AggregateId = id;
}
public override bool IsValid()
{
ValidationResult = new RemoveCustomerCommandValidation().Validate(this);
return ValidationResult.IsValid;
}
}
}
================================================
FILE: src/Equinox.Domain/Commands/UpdateCustomerCommand.cs
================================================
using System;
using Equinox.Domain.Commands.Validations;
namespace Equinox.Domain.Commands
{
public class UpdateCustomerCommand : CustomerCommand
{
public UpdateCustomerCommand(Guid id, string name, string email, DateTime birthDate)
{
Id = id;
Name = name;
Email = email;
BirthDate = birthDate;
}
public override bool IsValid()
{
ValidationResult = new UpdateCustomerCommandValidation().Validate(this);
return ValidationResult.IsValid;
}
}
}
================================================
FILE: src/Equinox.Domain/Commands/Validations/CustomerValidation.cs
================================================
using System;
using FluentValidation;
namespace Equinox.Domain.Commands.Validations
{
public abstract class CustomerValidation : AbstractValidator where T : CustomerCommand
{
protected void ValidateName()
{
RuleFor(c => c.Name)
.NotEmpty().WithMessage("Please ensure you have entered the Name")
.Length(2, 150).WithMessage("The Name must have between 2 and 150 characters");
}
protected void ValidateBirthDate()
{
RuleFor(c => c.BirthDate)
.NotEmpty()
.Must(HaveMinimumAge)
.WithMessage("The customer must have 18 years or more");
}
protected void ValidateEmail()
{
RuleFor(c => c.Email)
.NotEmpty()
.EmailAddress();
}
protected void ValidateId()
{
RuleFor(c => c.Id)
.NotEqual(Guid.Empty);
}
protected static bool HaveMinimumAge(DateTime birthDate)
{
return birthDate <= DateTime.Now.AddYears(-18);
}
}
}
================================================
FILE: src/Equinox.Domain/Commands/Validations/RegisterNewCustomerCommandValidation.cs
================================================
namespace Equinox.Domain.Commands.Validations
{
public class RegisterNewCustomerCommandValidation : CustomerValidation
{
public RegisterNewCustomerCommandValidation()
{
ValidateName();
ValidateBirthDate();
ValidateEmail();
}
}
}
================================================
FILE: src/Equinox.Domain/Commands/Validations/RemoveCustomerCommandValidation.cs
================================================
namespace Equinox.Domain.Commands.Validations
{
public class RemoveCustomerCommandValidation : CustomerValidation
{
public RemoveCustomerCommandValidation()
{
ValidateId();
}
}
}
================================================
FILE: src/Equinox.Domain/Commands/Validations/UpdateCustomerCommandValidation.cs
================================================
namespace Equinox.Domain.Commands.Validations
{
public class UpdateCustomerCommandValidation : CustomerValidation
{
public UpdateCustomerCommandValidation()
{
ValidateId();
ValidateName();
ValidateBirthDate();
ValidateEmail();
}
}
}
================================================
FILE: src/Equinox.Domain/Equinox.Domain.csproj
================================================
net9.0
disable
================================================
FILE: src/Equinox.Domain/Events/CustomerEventHandler.cs
================================================
using NetDevPack.SimpleMediator.Core.Interfaces;
using System.Threading;
using System.Threading.Tasks;
namespace Equinox.Domain.Events
{
public class CustomerEventHandler :
INotificationHandler,
INotificationHandler,
INotificationHandler
{
public Task Handle(CustomerUpdatedEvent message, CancellationToken cancellationToken)
{
// Send some notification e-mail
return Task.CompletedTask;
}
public Task Handle(CustomerRegisteredEvent message, CancellationToken cancellationToken)
{
// Send some greetings e-mail
return Task.CompletedTask;
}
public Task Handle(CustomerRemovedEvent message, CancellationToken cancellationToken)
{
// Send some see you soon e-mail
return Task.CompletedTask;
}
}
}
================================================
FILE: src/Equinox.Domain/Events/CustomerRegisteredEvent.cs
================================================
using System;
using NetDevPack.Messaging;
namespace Equinox.Domain.Events
{
public class CustomerRegisteredEvent : Event
{
public CustomerRegisteredEvent(Guid id, string name, string email, DateTime birthDate)
{
Id = id;
Name = name;
Email = email;
BirthDate = birthDate;
AggregateId = id;
}
public Guid Id { get; set; }
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime BirthDate { get; private set; }
}
}
================================================
FILE: src/Equinox.Domain/Events/CustomerRemovedEvent.cs
================================================
using System;
using NetDevPack.Messaging;
namespace Equinox.Domain.Events
{
public class CustomerRemovedEvent : Event
{
public CustomerRemovedEvent(Guid id)
{
Id = id;
AggregateId = id;
}
public Guid Id { get; set; }
}
}
================================================
FILE: src/Equinox.Domain/Events/CustomerUpdatedEvent.cs
================================================
using System;
using NetDevPack.Messaging;
namespace Equinox.Domain.Events
{
public class CustomerUpdatedEvent : Event
{
public CustomerUpdatedEvent(Guid id, string name, string email, DateTime birthDate)
{
Id = id;
Name = name;
Email = email;
BirthDate = birthDate;
AggregateId = id;
}
public Guid Id { get; set; }
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime BirthDate { get; private set; }
}
}
================================================
FILE: src/Equinox.Domain/Interfaces/ICustomerRepository.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Equinox.Domain.Models;
using NetDevPack.Data;
namespace Equinox.Domain.Interfaces
{
public interface ICustomerRepository : IRepository
{
Task GetById(Guid id);
Task GetByEmail(string email);
Task> GetAll();
void Add(Customer customer);
void Update(Customer customer);
void Remove(Customer customer);
}
}
================================================
FILE: src/Equinox.Domain/Models/Customer.cs
================================================
using System;
using NetDevPack.Domain;
namespace Equinox.Domain.Models
{
public class Customer : Entity, IAggregateRoot
{
public Customer(Guid id, string name, string email, DateTime birthDate)
{
Id = id;
Name = name;
Email = email;
BirthDate = birthDate;
}
// Empty constructor for EF
protected Customer() { }
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime BirthDate { get; private set; }
}
}
================================================
FILE: src/Equinox.Domain.Core/Equinox.Domain.Core.csproj
================================================
net9.0
disable
================================================
FILE: src/Equinox.Domain.Core/Events/IEventStore.cs
================================================
using NetDevPack.Messaging;
namespace Equinox.Domain.Core.Events
{
public interface IEventStore
{
void Save(T theEvent) where T : Event;
}
}
================================================
FILE: src/Equinox.Domain.Core/Events/StoredEvent.cs
================================================
using System;
using NetDevPack.Messaging;
namespace Equinox.Domain.Core.Events
{
public class StoredEvent : Event
{
public StoredEvent(Event theEvent, string data, string user)
{
Id = Guid.NewGuid();
AggregateId = theEvent.AggregateId;
MessageType = theEvent.MessageType;
Data = data;
User = user;
}
// EF Constructor
protected StoredEvent() { }
public Guid Id { get; private set; }
public string Data { get; private set; }
public string User { get; private set; }
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Bus/Equinox.Infra.CrossCutting.Bus.csproj
================================================
net9.0
disable
================================================
FILE: src/Equinox.Infra.CrossCutting.Bus/InMemoryBus.cs
================================================
using System.Threading.Tasks;
using Equinox.Domain.Core.Events;
using FluentValidation.Results;
using NetDevPack.Mediator;
using NetDevPack.Messaging;
using NetDevPack.SimpleMediator.Core.Interfaces;
namespace Equinox.Infra.CrossCutting.Bus
{
public sealed class InMemoryBus(IEventStore eventStore, IMediator mediator) : IMediatorHandler
{
private readonly IMediator _mediator = mediator;
private readonly IEventStore _eventStore = eventStore;
public async Task PublishEvent(T @event) where T : Event
{
if (!@event.MessageType.Equals("DomainNotification"))
_eventStore?.Save(@event);
await _mediator.Publish(@event);
}
public async Task SendCommand(T command) where T : Command
{
return await _mediator.Send(command);
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/API/AppJwtSettings.cs
================================================
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";
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/API/JwtBuilder.cs
================================================
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 { }
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizationValidation.cs
================================================
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));
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Authorization/CustomAuthorizeAttribute.cs
================================================
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) };
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Authorization/RequerimentClaimFilter.cs
================================================
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);
}
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Configuration/AspNetIdentityConfig.cs
================================================
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.Extensions.Hosting;
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)
{
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")));
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;
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Data/EquinoxIdentityContext.cs
================================================
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
{
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);
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Equinox.Infra.CrossCutting.Identity.csproj
================================================
net9.0
disable
all
runtime; build; native; contentfiles; analyzers; buildtransitive
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Extensions/ClaimsPrincipalExtensions.cs
================================================
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;
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.Designer.cs
================================================
//
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
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Migrations/20250408033115_SQLite.cs
================================================
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");
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Migrations/EquinoxIdentityContextModelSnapshot.cs
================================================
//
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
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Models/LoginUser.cs
================================================
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; }
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Models/RegisterUser.cs
================================================
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; }
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Models/UserClaim.cs
================================================
namespace Equinox.Infra.CrossCutting.Identity.Models
{
public class UserClaim
{
public string Value { get; set; }
public string Type { get; set; }
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Models/UserResponse.cs
================================================
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; }
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/Models/UserToken.cs
================================================
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; }
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/User/AspNetUser.cs
================================================
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 IsAuthenticated() ? Guid.Parse(_accessor.HttpContext.User.GetUserId()) : Guid.Empty;
}
public string GetUserEmail()
{
return IsAuthenticated() ? _accessor.HttpContext.User.GetUserEmail() : "";
}
public bool IsAuthenticated()
{
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;
}
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.Identity/User/IAspNetUser.cs
================================================
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 IsAuthenticated();
bool IsInRole(string role);
IEnumerable GetUserClaims();
HttpContext GetHttpContext();
}
}
================================================
FILE: src/Equinox.Infra.CrossCutting.IoC/Equinox.Infra.CrossCutting.IoC.csproj
================================================
net9.0
disable
================================================
FILE: src/Equinox.Infra.CrossCutting.IoC/NativeInjectorBootStrapper.cs
================================================
using Equinox.Application.Interfaces;
using Equinox.Application.Services;
using Equinox.Domain.Commands;
using Equinox.Domain.Core.Events;
using Equinox.Domain.Events;
using Equinox.Domain.Interfaces;
using Equinox.Infra.CrossCutting.Bus;
using Equinox.Infra.Data.Context;
using Equinox.Infra.Data.EventSourcing;
using Equinox.Infra.Data.Repository;
using Equinox.Infra.Data.Repository.EventSourcing;
using FluentValidation.Results;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using NetDevPack.Mediator;
using NetDevPack.SimpleMediator.Core.Implementation;
using NetDevPack.SimpleMediator.Core.Interfaces;
namespace Equinox.Infra.CrossCutting.IoC
{
public static class NativeInjectorBootStrapper
{
public static void RegisterServices(WebApplicationBuilder builder)
{
// Domain Bus (Mediator)
builder.Services.AddScoped();
builder.Services.AddScoped();
// Application
builder.Services.AddScoped();
// Domain - Events
builder.Services.AddScoped, CustomerEventHandler>();
builder.Services.AddScoped, CustomerEventHandler>();
builder.Services.AddScoped, CustomerEventHandler>();
// Domain - Commands
builder.Services.AddScoped, CustomerCommandHandler>();
builder.Services.AddScoped, CustomerCommandHandler>();
builder.Services.AddScoped, CustomerCommandHandler>();
// Infra - Data
builder.Services.AddScoped();
builder.Services.AddScoped();
// Infra - Data EventSourcing
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Context/EquinoxContext.cs
================================================
using System.Linq;
using System.Threading.Tasks;
using Equinox.Domain.Models;
using Equinox.Infra.Data.Mappings;
using FluentValidation.Results;
using Microsoft.EntityFrameworkCore;
using NetDevPack.Data;
using NetDevPack.Domain;
using NetDevPack.Mediator;
using NetDevPack.Messaging;
namespace Equinox.Infra.Data.Context
{
public sealed class EquinoxContext : DbContext, IUnitOfWork
{
private readonly IMediatorHandler _mediatorHandler;
public EquinoxContext(DbContextOptions options, IMediatorHandler mediatorHandler) : base(options)
{
_mediatorHandler = mediatorHandler;
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.AutoDetectChangesEnabled = false;
}
public DbSet Customers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore();
modelBuilder.Ignore();
foreach (var property in modelBuilder.Model.GetEntityTypes().SelectMany(
e => e.GetProperties().Where(p => p.ClrType == typeof(string))))
property.SetColumnType("varchar(100)");
modelBuilder.ApplyConfiguration(new CustomerMap());
base.OnModelCreating(modelBuilder);
}
public async Task Commit()
{
// Dispatch Domain Events collection.
// Choices:
// A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including
// side effects from the domain event handlers which are using the same DbContext with "InstancePerLifetimeScope" or "scoped" lifetime
// B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions.
// You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers.
await _mediatorHandler.PublishDomainEvents(this).ConfigureAwait(false);
// After executing this line all the changes (from the Command Handler and Domain Event Handlers)
// performed through the DbContext will be committed
var success = await SaveChangesAsync() > 0;
return success;
}
}
public static class MediatorExtension
{
public static async Task PublishDomainEvents(this IMediatorHandler mediator, T ctx) where T : DbContext
{
var domainEntities = ctx.ChangeTracker
.Entries()
.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());
var domainEvents = domainEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList();
domainEntities.ToList()
.ForEach(entity => entity.Entity.ClearDomainEvents());
var tasks = domainEvents
.Select(async (domainEvent) => {
await mediator.PublishEvent(domainEvent);
});
await Task.WhenAll(tasks);
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Context/EventStoreSQLContext.cs
================================================
using Equinox.Domain.Core.Events;
using Equinox.Infra.Data.Mappings;
using Microsoft.EntityFrameworkCore;
namespace Equinox.Infra.Data.Context
{
public class EventStoreSqlContext : DbContext
{
public EventStoreSqlContext(DbContextOptions options) : base(options) { }
public DbSet StoredEvent { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new StoredEventMap());
base.OnModelCreating(modelBuilder);
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Equinox.Infra.Data.csproj
================================================
net9.0
disable
================================================
FILE: src/Equinox.Infra.Data/EventSourcing/SqlEventStore.cs
================================================
using Equinox.Domain.Core.Events;
using Equinox.Infra.CrossCutting.Identity.User;
using Equinox.Infra.Data.Repository.EventSourcing;
using NetDevPack.Messaging;
using Newtonsoft.Json;
namespace Equinox.Infra.Data.EventSourcing
{
public class SqlEventStore : IEventStore
{
private readonly IEventStoreRepository _eventStoreRepository;
private readonly IAspNetUser _user;
public SqlEventStore(IEventStoreRepository eventStoreRepository, IAspNetUser user)
{
_eventStoreRepository = eventStoreRepository;
_user = user;
}
public void Save(T theEvent) where T : Event
{
// Using Newtonsoft.Json because System.Text.Json
// is a sad joke to be considered "Done"
// The System.Text don't know how serialize a
// object with inherited properties, I said is sad...
// Yes! I tried: options = new JsonSerializerOptions { WriteIndented = true };
var serializedData = JsonConvert.SerializeObject(theEvent);
var storedEvent = new StoredEvent(
theEvent,
serializedData,
_user.Name ?? _user.GetUserEmail());
_eventStoreRepository.Store(storedEvent);
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Mappings/CustomerMap.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Equinox.Domain.Models;
namespace Equinox.Infra.Data.Mappings
{
public class CustomerMap : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder.Property(c => c.Id)
.HasColumnName("Id");
builder.Property(c => c.Name)
.HasColumnType("varchar(100)")
.HasMaxLength(100)
.IsRequired();
builder.Property(c => c.Email)
.HasColumnType("varchar(100)")
.HasMaxLength(100)
.IsRequired();
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Mappings/StoredEventMap.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Equinox.Domain.Core.Events;
namespace Equinox.Infra.Data.Mappings
{
public class StoredEventMap : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder.Property(c => c.Timestamp)
.HasColumnName("CreationDate");
builder.Property(c => c.MessageType)
.HasColumnName("Action")
.HasColumnType("varchar(100)");
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.Designer.cs
================================================
//
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
{
[DbContext(typeof(EquinoxContext))]
[Migration("20250408031104_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.Models.Customer", b =>
{
b.Property("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("Id");
b.Property("BirthDate")
.HasColumnType("TEXT");
b.Property("Email")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id");
b.ToTable("Customers");
});
#pragma warning restore 612, 618
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Migrations/20250408031104_SQLite.cs
================================================
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Equinox.Infra.Data.Migrations
{
///
public partial class SQLite : Migration
{
///
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Customers",
columns: table => new
{
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 =>
{
table.PrimaryKey("PK_Customers", x => x.Id);
});
}
///
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Customers");
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Migrations/EquinoxContextModelSnapshot.cs
================================================
//
using System;
using Equinox.Infra.Data.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Equinox.Infra.Data.Migrations
{
[DbContext(typeof(EquinoxContext))]
partial class EquinoxContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#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")
.HasColumnType("TEXT");
b.Property("Email")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id");
b.ToTable("Customers");
});
#pragma warning restore 612, 618
}
}
}
================================================
FILE: src/Equinox.Infra.Data/Migrations/EventStoreSQL/EventStoreSQLContextModelSnapshot.cs
================================================
//
using System;
using Equinox.Infra.Data.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Equinox.Infra.Data.Migrations.EventStoreSql
{
[DbContext(typeof(EventStoreSqlContext))]
partial class EventStoreSqlContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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