Initial code commit.
This commit is contained in:
53
.dockerignore
Normal file
53
.dockerignore
Normal file
@@ -0,0 +1,53 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# IDE
|
||||
.vs
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Build artifacts
|
||||
bin/
|
||||
obj/
|
||||
out/
|
||||
dist/
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
packages/
|
||||
.nuget/
|
||||
|
||||
# Node modules (for UI)
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Test
|
||||
test-results/
|
||||
coverage/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
.env
|
||||
|
||||
# CI/CD
|
||||
.github/
|
||||
.gitlab-ci.yml
|
||||
.travis.yml
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.cache
|
||||
46
.gitea/workflows/build.yml
Normal file
46
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Build & Test
|
||||
|
||||
permissions: read-all
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
types: [opened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🔎 Checkout files
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: 💎 Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
id: cache-nuget
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
~/.local/share/NuGet/Cache-*
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: 🎽 Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: 💫 Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: 🔥 Build
|
||||
run: dotnet build --no-restore -o "${{ vars.ARTIFACT_DIR }}/output"
|
||||
|
||||
- name: 📈 Test
|
||||
run: dotnet test --verbosity normal
|
||||
|
||||
|
||||
103
.gitea/workflows/deploy.yml
Normal file
103
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
name: .NET
|
||||
permissions: read-all
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
APPSETTINGS_PATH: ui/Sufi.Demo.PeopleDirectory.UI/Server/appsettings.json
|
||||
IMAGE_NAME: sufiaziz/demo-contact
|
||||
steps:
|
||||
- name: 🔧 Install dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl sed ca-certificates gnupg
|
||||
|
||||
# Install Docker CLI with Compose plugin
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||||
tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
apt-get update
|
||||
apt-get install -y docker-ce-cli docker-compose-plugin
|
||||
|
||||
# Verify installation
|
||||
docker compose version
|
||||
|
||||
- name: 🔎 Checkout files
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: 💎 Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
id: cache-nuget
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
~/.local/share/NuGet/Cache-*
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: 🎽 Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: 🔄 Replace placeholders in docker-compose.yml
|
||||
run: |
|
||||
echo "Replacing placeholders in docker-compose.yml..."
|
||||
echo "Replace connection string..."
|
||||
sed -i "s|#ConnectionString#|${{ secrets.CONNECTION_STRING }}|g" docker-compose.yml
|
||||
echo "Replace license key..."
|
||||
sed -i "s|#LuckyPennyLicenseKey#|${{ secrets.LUCKY_PENNY_LICENSE_KEY }}|g" docker-compose.yml
|
||||
echo "Replace SEQ settings..."
|
||||
sed -i "s|#SEQ_URL#|${{ vars.SEQ_URL }}|g" docker-compose.yml
|
||||
sed -i "s|#SEQ_APIKEY#|${{ secrets.SEQ_APIKEY }}|g" docker-compose.yml
|
||||
|
||||
- name: 🐳 Build Docker image
|
||||
run: |
|
||||
docker build -t $IMAGE_NAME:latest \
|
||||
--build-arg BUILD_PROJ="${{ vars.BUILD_PROJ }}" \
|
||||
-f Dockerfile .
|
||||
echo "Docker image built successfully"
|
||||
|
||||
- name: 🛑 Stop existing containers
|
||||
run: |
|
||||
echo "Stopping existing containers via docker compose..."
|
||||
docker compose down || echo "No existing containers to stop"
|
||||
|
||||
- name: 🚀 Start containers with docker compose
|
||||
run: |
|
||||
echo "Starting containers via docker compose..."
|
||||
docker compose up -d
|
||||
echo "Containers started successfully at $(date)"
|
||||
|
||||
- name: ✅ Verify container status
|
||||
run: |
|
||||
echo "Waiting for containers to be healthy..."
|
||||
sleep 5
|
||||
|
||||
echo "Listing running containers:"
|
||||
docker compose ps
|
||||
|
||||
echo ""
|
||||
echo "Recent logs:"
|
||||
docker compose logs --tail=20
|
||||
|
||||
# Check if main service is running
|
||||
if docker compose ps | grep -q "Up"; then
|
||||
echo "Containers are running successfully"
|
||||
else
|
||||
echo "Error: Containers failed to start"
|
||||
docker compose logs
|
||||
exit 1
|
||||
fi
|
||||
399
.gitignore
vendored
Normal file
399
.gitignore
vendored
Normal file
@@ -0,0 +1,399 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# 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
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# 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/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# 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
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.env
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
|
||||
WORKDIR /App
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN dotnet restore
|
||||
|
||||
RUN dotnet publish "ui/Sufi.Demo.PeopleDirectory.UI/Server/Sufi.Demo.PeopleDirectory.UI.Server.csproj" -c Release -o out
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
|
||||
|
||||
WORKDIR /App
|
||||
|
||||
COPY --from=build /App/out ./
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["dotnet", "Sufi.Demo.PeopleDirectory.UI.Server.dll"]
|
||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 yoimili
|
||||
|
||||
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.
|
||||
@@ -1,2 +1,3 @@
|
||||
# demo-contact
|
||||
|
||||
A simple demonstration on my coding knowledge and capabilities. In case someone need to see it :)
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Common
|
||||
{
|
||||
public interface IService
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories
|
||||
{
|
||||
public interface IAsyncRepository<T, in TId> where T : class, IEntity<TId>
|
||||
{
|
||||
IQueryable<T> Entities { get; }
|
||||
|
||||
Task<T?> GetByIdAsync(TId id);
|
||||
|
||||
Task<List<T>> GetAllAsync();
|
||||
|
||||
Task<List<T>> GetPagedResponseAsync(int pageNumber, int pageSize);
|
||||
|
||||
Task<T> AddAsync(T entity);
|
||||
|
||||
Task UpdateAsync(T entity);
|
||||
|
||||
Task DeleteAsync(T entity);
|
||||
Task<int> DeleteByIdAsync(TId id);
|
||||
|
||||
Task<int> CountAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories
|
||||
{
|
||||
public interface IUnitOfWork<TId> : IDisposable
|
||||
{
|
||||
IAsyncRepository<T, TId> Repository<T>() where T : AuditableEntity<TId>;
|
||||
|
||||
Task<int> Commit(CancellationToken cancellationToken);
|
||||
|
||||
Task<int> CommitAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys);
|
||||
|
||||
Task Rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Services
|
||||
{
|
||||
public interface IAppCache
|
||||
{
|
||||
ValueTask<T> GetOrAddAsync<T>(string key, Func<CancellationToken, ValueTask<T>> factory,
|
||||
IEnumerable<string>? tags = null, TimeSpan? absoluteExpireTime = null);
|
||||
ValueTask RemoveAsync(string key);
|
||||
ValueTask RemoveByTagAsync(string tag);
|
||||
ValueTask ResetAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Common;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Contracts.Services
|
||||
{
|
||||
public interface ICurrentUserService : IService
|
||||
{
|
||||
string? UserId { get; }
|
||||
}
|
||||
}
|
||||
10
Sufi.Demo.PeopleDirectory.Application/Enums/AuditType.cs
Normal file
10
Sufi.Demo.PeopleDirectory.Application/Enums/AuditType.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Enums
|
||||
{
|
||||
public enum AuditType
|
||||
{
|
||||
None = 0,
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
Delete = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Extensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddApplicationLayer(this IServiceCollection services, string licenseKey)
|
||||
{
|
||||
services.AddAutoMapper(config =>
|
||||
{
|
||||
config.LicenseKey = licenseKey;
|
||||
config.AddMaps(Assembly.GetExecutingAssembly());
|
||||
});
|
||||
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.LicenseKey = licenseKey;
|
||||
config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||
});
|
||||
|
||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands
|
||||
{
|
||||
public class AddEditContactCommand : IRequest<IResult<int>>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = Random.Shared.Next(1000000000, 1999999999).ToString();
|
||||
public string Email { get; set; } = "user@example.com";
|
||||
public string SkillSets { get; set; } = "skill1, skill2, skill3";
|
||||
public string Hobby { get; set; } = "Hobby";
|
||||
}
|
||||
|
||||
public sealed class AddEditContactCommandValidator : AbstractValidator<AddEditContactCommand>
|
||||
{
|
||||
public AddEditContactCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.UserName)
|
||||
.NotEmpty().WithMessage("UserName is required.")
|
||||
.MaximumLength(50).WithMessage("UserName must not exceed 50 characters.");
|
||||
RuleFor(v => v.Phone)
|
||||
.NotEmpty().WithMessage("Phone is required.")
|
||||
.MaximumLength(20).WithMessage("Phone must not exceed 20 characters.");
|
||||
RuleFor(v => v.Email)
|
||||
.NotEmpty().WithMessage("Email is required.")
|
||||
.EmailAddress().WithMessage("A valid email is required.")
|
||||
.MaximumLength(100)
|
||||
.WithMessage("Email must not exceed 100 characters.");
|
||||
RuleFor(v => v.SkillSets)
|
||||
.NotEmpty().WithMessage("SkillSets is required.")
|
||||
.MaximumLength(255).WithMessage("SkillSets must not exceed 255 characters.");
|
||||
RuleFor(v => v.Hobby)
|
||||
.NotEmpty().WithMessage("Hobby is required.")
|
||||
.MaximumLength(255).WithMessage("Hobby must not exceed 255 characters.");
|
||||
}
|
||||
}
|
||||
|
||||
public class AddEditContactCommandHandler(
|
||||
IMapper mapper,
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
ILogger<AddEditContactCommandHandler> logger,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<AddEditContactCommand, IResult<int>>
|
||||
{
|
||||
public async Task<IResult<int>> Handle(AddEditContactCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.Id == 0)
|
||||
{
|
||||
// Only add if max count is not more than 100.
|
||||
var count = await unitOfWork.Repository<Contact>().CountAsync();
|
||||
if (count > 100)
|
||||
{
|
||||
return await Result<int>.FailAsync("Max item count reached. Please delete some first.");
|
||||
}
|
||||
|
||||
var contact = mapper.Map<Contact>(command);
|
||||
await unitOfWork.Repository<Contact>().AddAsync(contact);
|
||||
await unitOfWork.Commit(cancellationToken);
|
||||
|
||||
// Invalidate cache.
|
||||
await appCache.RemoveAsync("contact_all");
|
||||
|
||||
logger.LogInformation("New contact added with ID: {Id}", contact.Id);
|
||||
|
||||
return await Result<int>.SuccessAsync(contact.Id, "New contact saved.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var contact = await unitOfWork.Repository<Contact>().GetByIdAsync(command.Id);
|
||||
if (contact != null)
|
||||
{
|
||||
mapper.Map(command, contact);
|
||||
|
||||
await unitOfWork.Repository<Contact>().UpdateAsync(contact);
|
||||
await unitOfWork.Commit(cancellationToken);
|
||||
|
||||
// Invalidate cache.
|
||||
await appCache.RemoveAsync($"contact_{command.Id}");
|
||||
await appCache.RemoveAsync("contact_all");
|
||||
|
||||
logger.LogInformation("Contact updated with ID: {Id}", contact.Id);
|
||||
|
||||
return await Result<int>.SuccessAsync(contact.Id, "Contact updated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("Contact not found with ID: {Id}", command.Id);
|
||||
|
||||
return await Result<int>.FailAsync("Contact not found!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands
|
||||
{
|
||||
public class DeleteContactCommand : IRequest<IResult>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DeleteContactCommandValidator : AbstractValidator<DeleteContactCommand>
|
||||
{
|
||||
public DeleteContactCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.Id)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("A valid Id is required.");
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteContactCommandHandler(
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
ILogger<DeleteContactCommandHandler> logger,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<DeleteContactCommand, IResult>
|
||||
{
|
||||
public async Task<IResult> Handle(DeleteContactCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var itemToDelete = await unitOfWork.Repository<Contact>().GetByIdAsync(request.Id);
|
||||
if (itemToDelete != null)
|
||||
{
|
||||
await unitOfWork.Repository<Contact>().DeleteByIdAsync(request.Id);
|
||||
|
||||
// Clear cache entries related to contacts.
|
||||
await appCache.RemoveAsync($"contact_{request.Id}");
|
||||
await appCache.RemoveAsync("contact_all");
|
||||
|
||||
logger.LogInformation("Contact with ID: {Id} deleted.", request.Id);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
logger.LogWarning("No contact found with ID: {Id}", request.Id);
|
||||
|
||||
return Result.Fail("No data to delete.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using AutoMapper;
|
||||
using MediatR;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll
|
||||
{
|
||||
public class GetAllContactsQuery : IRequest<IResult<List<GetAllContactsResponse>>>
|
||||
{
|
||||
}
|
||||
|
||||
public class GetAllContactsQueryHandler(
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
IMapper mapper,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<GetAllContactsQuery, IResult<List<GetAllContactsResponse>>>
|
||||
{
|
||||
public async Task<IResult<List<GetAllContactsResponse>>> Handle(GetAllContactsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
Task<List<Contact>> allContactsFunc() => unitOfWork.Repository<Contact>().GetAllAsync();
|
||||
|
||||
var allContacts = await appCache.GetOrAddAsync(
|
||||
"contact_all",
|
||||
async token => await allContactsFunc(),
|
||||
absoluteExpireTime: TimeSpan.FromMinutes(2),
|
||||
tags: ["contacts"]
|
||||
);
|
||||
|
||||
var mappedContacts = mapper.Map<List<GetAllContactsResponse>>(allContacts);
|
||||
return await Result<List<GetAllContactsResponse>>.SuccessAsync(mappedContacts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll
|
||||
{
|
||||
public record GetAllContactsResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string SkillSets { get; set; } = null!;
|
||||
public string Hobby { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById
|
||||
{
|
||||
public class GetContactByIdQuery : IRequest<IResult<GetContactByIdResponse>>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public sealed class GetContactByIdQueryValidator : AbstractValidator<GetContactByIdQuery>
|
||||
{
|
||||
public GetContactByIdQueryValidator()
|
||||
{
|
||||
RuleFor(v => v.Id)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("A valid Id is required.");
|
||||
}
|
||||
}
|
||||
|
||||
public class GetContactByIdQueryHandler(
|
||||
IUnitOfWork<int> unitOfWork,
|
||||
IMapper mapper,
|
||||
IAppCache appCache
|
||||
) : IRequestHandler<GetContactByIdQuery, IResult<GetContactByIdResponse>>
|
||||
{
|
||||
public async Task<IResult<GetContactByIdResponse>> Handle(GetContactByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
Task<Contact?> getContactByIdFunc() => unitOfWork.Repository<Contact>().GetByIdAsync(request.Id);
|
||||
|
||||
var contact = await appCache.GetOrAddAsync(
|
||||
$"contact_{request.Id}",
|
||||
async token => await getContactByIdFunc(),
|
||||
absoluteExpireTime: TimeSpan.FromMinutes(2),
|
||||
tags: ["contacts"]
|
||||
);
|
||||
|
||||
var mappedContact = mapper.Map<GetContactByIdResponse>(contact);
|
||||
return await Result<GetContactByIdResponse>.SuccessAsync(mappedContact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById
|
||||
{
|
||||
public record GetContactByIdResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string SkillSets { get; set; } = null!;
|
||||
public string Hobby { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using AutoMapper;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Responses;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Mappings
|
||||
{
|
||||
public class ContactProfile : Profile
|
||||
{
|
||||
public ContactProfile()
|
||||
{
|
||||
CreateMap<AddEditContactCommand, Contact>().ReverseMap();
|
||||
CreateMap<GetAllContactsResponse, Contact>().ReverseMap();
|
||||
CreateMap<Contact, GetContactByIdResponse>();
|
||||
CreateMap<Contact, ContactResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.Application.Responses
|
||||
{
|
||||
public class ContactResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Phone { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string SkillSets { get; set; } = null!;
|
||||
public string Hobby { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="15.0.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="MediatR" Version="13.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Domain\Sufi.Demo.PeopleDirectory.Domain.csproj" />
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Shared\Sufi.Demo.PeopleDirectory.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Features\Boomerang\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
13
Sufi.Demo.PeopleDirectory.Domain/Common/AuditableEntity.cs
Normal file
13
Sufi.Demo.PeopleDirectory.Domain/Common/AuditableEntity.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Domain.Common
|
||||
{
|
||||
public abstract class AuditableEntity<TId> : IAuditableEntity<TId>
|
||||
{
|
||||
public TId Id { get; set; } = default!;
|
||||
public string? CreatedBy { get; set; }
|
||||
public DateTime CreatedOn { get; set; }
|
||||
public string? LastModifiedBy { get; set; }
|
||||
public DateTime? LastModifiedOn { get; set; }
|
||||
}
|
||||
}
|
||||
17
Sufi.Demo.PeopleDirectory.Domain/Common/IAuditableEntity.cs
Normal file
17
Sufi.Demo.PeopleDirectory.Domain/Common/IAuditableEntity.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Domain.Common
|
||||
{
|
||||
public interface IAuditableEntity<TId> : IAuditableEntity, IEntity<TId>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public interface IAuditableEntity : IEntity
|
||||
{
|
||||
string? CreatedBy { get; set; }
|
||||
DateTime CreatedOn { get; set; }
|
||||
string? LastModifiedBy { get; set; }
|
||||
DateTime? LastModifiedOn { get; set; }
|
||||
}
|
||||
}
|
||||
14
Sufi.Demo.PeopleDirectory.Domain/Common/IEntity.cs
Normal file
14
Sufi.Demo.PeopleDirectory.Domain/Common/IEntity.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Domain.Common
|
||||
{
|
||||
public interface IEntity
|
||||
{
|
||||
}
|
||||
|
||||
public interface IEntity<TId> : IEntity
|
||||
{
|
||||
[Key]
|
||||
TId Id { get; set; }
|
||||
}
|
||||
}
|
||||
27
Sufi.Demo.PeopleDirectory.Domain/Entities/Misc/Contact.cs
Normal file
27
Sufi.Demo.PeopleDirectory.Domain/Entities/Misc/Contact.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Domain.Entities.Misc
|
||||
{
|
||||
public class Contact : AuditableEntity<int>
|
||||
{
|
||||
[Required]
|
||||
[Column(TypeName = "character varying(50)")]
|
||||
public string UserName { get; set; } = null!;
|
||||
[Required]
|
||||
[Phone]
|
||||
[Column(TypeName = "character varying(20)")]
|
||||
public string Phone { get; set; } = null!;
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Column(TypeName = "character varying(100)")]
|
||||
public string Email { get; set; } = null!;
|
||||
[Required]
|
||||
[Column(TypeName = "character varying(255)")]
|
||||
public string SkillSets { get; set; } = null!;
|
||||
[Required]
|
||||
[Column(TypeName = "character varying(255)")]
|
||||
public string Hobby { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
13
Sufi.Demo.PeopleDirectory.Domain/Entities/Misc/ServerInfo.cs
Normal file
13
Sufi.Demo.PeopleDirectory.Domain/Entities/Misc/ServerInfo.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Domain.Entities.Misc
|
||||
{
|
||||
public class ServerInfo : AuditableEntity<string>
|
||||
{
|
||||
[Required]
|
||||
[Column(TypeName = "character varying(255)")]
|
||||
public string Value { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Infrastructure.Identity
|
||||
{
|
||||
public class CurrentUserService : ICurrentUserService
|
||||
{
|
||||
public string? UserId { get; }
|
||||
public List<KeyValuePair<string, string>>? Claims { get; set; }
|
||||
|
||||
public CurrentUserService(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
var userClaim = httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (userClaim != null)
|
||||
{
|
||||
UserId = userClaim.Value;
|
||||
}
|
||||
|
||||
Claims = httpContextAccessor.HttpContext?.User?.Claims.AsEnumerable().Select(item => new KeyValuePair<string, string>(item.Type, item.Value)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Quartz;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Infrastructure.Identity;
|
||||
using Sufi.Demo.PeopleDirectory.Infrastructure.Jobs;
|
||||
using Sufi.Demo.PeopleDirectory.Infrastructure.Services;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Infrastructure
|
||||
{
|
||||
public static class InfrastructureServiceRegistration
|
||||
{
|
||||
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddHybridCache();
|
||||
|
||||
services.AddTransient<ICurrentUserService, CurrentUserService>()
|
||||
.AddTransient<IAppCache, AppCache>();
|
||||
|
||||
// Some background jobs here.
|
||||
services.AddQuartz(options =>
|
||||
{
|
||||
var jobKey = new JobKey("ClearPersistentDataJob");
|
||||
options.AddJob<ClearPersistentDataJob>(opt => opt.WithIdentity(jobKey));
|
||||
options.AddTrigger(opt =>
|
||||
{
|
||||
opt.ForJob(jobKey)
|
||||
.WithIdentity("ClearPersistentDataJob-trigger")
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInMinutes(10)
|
||||
.RepeatForever());
|
||||
});
|
||||
});
|
||||
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Quartz;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Infrastructure.Jobs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the cleanup job.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initialize an instance of <see cref="ClearPersistentDataJob"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="unitOfWorkInt"></param>
|
||||
/// <param name="unitOfWorkString"></param>
|
||||
public class ClearPersistentDataJob(IUnitOfWork<int> unitOfWorkInt, IUnitOfWork<string> unitOfWorkString, IAppCache appCache) : IJob
|
||||
{
|
||||
private const string LastDateDeletedKey = "LastDateDeleted";
|
||||
private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss.ffff";
|
||||
|
||||
/// <summary>
|
||||
/// Job implementation to be executed.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
// Get all contacts to be deleted.
|
||||
var contactsToDelete = await unitOfWorkInt.Repository<Contact>().GetAllAsync();
|
||||
|
||||
// Skips if nothing to delete.
|
||||
if (contactsToDelete.Count == 0)
|
||||
return;
|
||||
|
||||
// Delete all contacts.
|
||||
foreach (var contact in contactsToDelete)
|
||||
{
|
||||
await unitOfWorkInt.Repository<Contact>().DeleteAsync(contact);
|
||||
}
|
||||
|
||||
// Update the last date deleted in the ServerInfo table.
|
||||
var infoToUpdate = await unitOfWorkString.Repository<ServerInfo>().GetByIdAsync(LastDateDeletedKey);
|
||||
if (infoToUpdate != null)
|
||||
{
|
||||
infoToUpdate.Value = DateTime.UtcNow.ToString(DateTimeFormat);
|
||||
await unitOfWorkString.Repository<ServerInfo>().UpdateAsync(infoToUpdate);
|
||||
}
|
||||
else
|
||||
{
|
||||
var infoToAdd = new ServerInfo { Id = LastDateDeletedKey, Value = DateTime.UtcNow.ToString(DateTimeFormat) };
|
||||
await unitOfWorkString.Repository<ServerInfo>().AddAsync(infoToAdd);
|
||||
}
|
||||
|
||||
// Commit the changes to the database.
|
||||
await unitOfWorkString.Commit(context.CancellationToken);
|
||||
|
||||
// Clear cache entries related to contacts.
|
||||
await appCache.ResetAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Infrastructure.Services
|
||||
{
|
||||
public class AppCache(
|
||||
HybridCache hybridCache
|
||||
) : IAppCache
|
||||
{
|
||||
public ValueTask<T> GetOrAddAsync<T>(string key, Func<CancellationToken, ValueTask<T>> factory,
|
||||
IEnumerable<string>? tags = null, TimeSpan? absoluteExpireTime = null)
|
||||
{
|
||||
var options = new HybridCacheEntryOptions
|
||||
{
|
||||
Expiration = absoluteExpireTime ?? TimeSpan.FromSeconds(30),
|
||||
};
|
||||
|
||||
return hybridCache.GetOrCreateAsync<T>(key, factory, options, tags);
|
||||
}
|
||||
|
||||
public ValueTask RemoveAsync(string key)
|
||||
{
|
||||
return hybridCache.RemoveAsync(key);
|
||||
}
|
||||
|
||||
public ValueTask RemoveByTagAsync(string tag)
|
||||
{
|
||||
return hybridCache.RemoveByTagAsync(tag);
|
||||
}
|
||||
|
||||
public ValueTask ResetAsync()
|
||||
{
|
||||
return hybridCache.RemoveByTagAsync("*");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.10.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Application\Sufi.Demo.PeopleDirectory.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Models.Identity;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Contexts
|
||||
{
|
||||
public class ApplicationDbContext(
|
||||
DbContextOptions<ApplicationDbContext> options,
|
||||
ICurrentUserService currentUserService
|
||||
) : AuditableContext(options)
|
||||
{
|
||||
public virtual DbSet<Contact> Contacts { get; set; } = null!;
|
||||
public virtual DbSet<ServerInfo> ServerInfos { get; set; } = null!;
|
||||
|
||||
public override Task<int> SaveChangesAsync(string? userId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
PopulateAuditRecords();
|
||||
|
||||
return base.SaveChangesAsync(userId, cancellationToken);
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
PopulateAuditRecords();
|
||||
|
||||
if (currentUserService.UserId == null)
|
||||
{
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return await base.SaveChangesAsync(currentUserService.UserId, cancellationToken);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
foreach (var property in builder.Model.GetEntityTypes()
|
||||
.SelectMany(t => t.GetProperties())
|
||||
.Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?)))
|
||||
{
|
||||
property.SetColumnType("decimal(18,2)");
|
||||
}
|
||||
|
||||
foreach (var property in builder.Model.GetEntityTypes()
|
||||
.SelectMany(t => t.GetProperties())
|
||||
.Where(p => p.Name is "LastModifiedBy" or "CreatedBy"))
|
||||
{
|
||||
property.SetColumnType("character varying(100)");
|
||||
}
|
||||
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
var assembly = typeof(ApplicationDbContext).Assembly;
|
||||
builder.ApplyConfigurationsFromAssembly(assembly);
|
||||
|
||||
builder.Entity<IdentityUserRole<string>>(entity => entity.ToTable("UserRoles", "Identity"));
|
||||
|
||||
builder.Entity<IdentityUserClaim<string>>(entity => entity.ToTable("UserClaims", "Identity"));
|
||||
|
||||
builder.Entity<IdentityUserLogin<string>>(entity => entity.ToTable("UserLogins", "Identity"));
|
||||
|
||||
builder.Entity<AppRoleClaim>(entity => entity.ToTable("RoleClaims", "Identity"));
|
||||
|
||||
builder.Entity<IdentityUserToken<string>>(entity => entity.ToTable("UserTokens", "Identity"));
|
||||
}
|
||||
|
||||
private void PopulateAuditRecords()
|
||||
{
|
||||
foreach (var entry in ChangeTracker.Entries<IAuditableEntity>().ToList())
|
||||
{
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
entry.Entity.CreatedOn = DateTime.UtcNow;
|
||||
entry.Entity.CreatedBy = currentUserService.UserId;
|
||||
break;
|
||||
|
||||
case EntityState.Modified:
|
||||
entry.Entity.LastModifiedOn = DateTime.UtcNow;
|
||||
entry.Entity.LastModifiedBy = currentUserService.UserId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Enums;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Models.Audit;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Models.Identity;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Contexts
|
||||
{
|
||||
public abstract class AuditableContext(
|
||||
DbContextOptions options
|
||||
) : IdentityDbContext<AppUser, AppRole, string, IdentityUserClaim<string>, IdentityUserRole<string>,
|
||||
IdentityUserLogin<string>, AppRoleClaim, IdentityUserToken<string>>(options)
|
||||
{
|
||||
public DbSet<Audit> AuditTrails { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
builder.Entity<Audit>().ToTable("AuditTrails", "Audit");
|
||||
}
|
||||
|
||||
public virtual async Task<int> SaveChangesAsync(string? userId = null, CancellationToken cancellationToken = new())
|
||||
{
|
||||
var auditEntries = OnBeforeSaveChanges(userId);
|
||||
var result = await base.SaveChangesAsync(cancellationToken);
|
||||
await OnAfterSaveChanges(auditEntries, cancellationToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<AuditEntry> OnBeforeSaveChanges(string? userId)
|
||||
{
|
||||
ChangeTracker.DetectChanges();
|
||||
var auditEntries = new List<AuditEntry>();
|
||||
foreach (var entry in ChangeTracker.Entries())
|
||||
{
|
||||
if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
|
||||
continue;
|
||||
|
||||
var auditEntry = new AuditEntry(entry)
|
||||
{
|
||||
TableName = entry.Entity.GetType().Name,
|
||||
UserId = userId
|
||||
};
|
||||
auditEntries.Add(auditEntry);
|
||||
foreach (var property in entry.Properties)
|
||||
{
|
||||
if (property.IsTemporary)
|
||||
{
|
||||
auditEntry.TemporaryProperties.Add(property);
|
||||
continue;
|
||||
}
|
||||
|
||||
string propertyName = property.Metadata.Name;
|
||||
if (property.Metadata.IsPrimaryKey())
|
||||
{
|
||||
auditEntry.KeyValues[propertyName] = property.CurrentValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
auditEntry.AuditType = AuditType.Create;
|
||||
auditEntry.NewValues[propertyName] = property.CurrentValue;
|
||||
break;
|
||||
|
||||
case EntityState.Deleted:
|
||||
auditEntry.AuditType = AuditType.Delete;
|
||||
auditEntry.OldValues[propertyName] = property.OriginalValue;
|
||||
break;
|
||||
|
||||
case EntityState.Modified:
|
||||
if (property.IsModified && property.OriginalValue?.Equals(property.CurrentValue) == false)
|
||||
{
|
||||
auditEntry.ChangedColumns.Add(propertyName);
|
||||
auditEntry.AuditType = AuditType.Update;
|
||||
auditEntry.OldValues[propertyName] = property.OriginalValue;
|
||||
auditEntry.NewValues[propertyName] = property.CurrentValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties))
|
||||
{
|
||||
AuditTrails.Add(auditEntry.ToAudit());
|
||||
}
|
||||
return [.. auditEntries.Where(_ => _.HasTemporaryProperties)];
|
||||
}
|
||||
|
||||
private Task OnAfterSaveChanges(List<AuditEntry> auditEntries, CancellationToken cancellationToken = new())
|
||||
{
|
||||
if (auditEntries == null || auditEntries.Count == 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
foreach (var auditEntry in auditEntries)
|
||||
{
|
||||
foreach (var prop in auditEntry.TemporaryProperties)
|
||||
{
|
||||
if (prop.Metadata.IsPrimaryKey())
|
||||
{
|
||||
auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
|
||||
}
|
||||
}
|
||||
AuditTrails.Add(auditEntry.ToAudit());
|
||||
}
|
||||
return SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Models.Identity;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Contexts.EntityMaps
|
||||
{
|
||||
public class AppRoleMap : IEntityTypeConfiguration<AppRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AppRole> builder)
|
||||
{
|
||||
builder.ToTable("Roles", "Identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Models.Identity;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Contexts.EntityMaps
|
||||
{
|
||||
public class AppUserMap : IEntityTypeConfiguration<AppUser>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AppUser> builder)
|
||||
{
|
||||
builder.ToTable("Users", "Identity");
|
||||
|
||||
builder.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Contexts;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Models.Identity;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Repositories;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Extensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddPersistenceServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql(configuration.GetConnectionString("DefaultConnectionString")!));
|
||||
|
||||
services
|
||||
.AddIdentityCore<AppUser>(options =>
|
||||
{
|
||||
options.Password.RequiredLength = 6;
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.User.RequireUniqueEmail = true;
|
||||
})
|
||||
.AddRoles<AppRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services
|
||||
.AddTransient(typeof(IAsyncRepository<,>), typeof(AsyncRepository<,>))
|
||||
.AddTransient(typeof(IUnitOfWork<>), typeof(UnitOfWork<>));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
457
Sufi.Demo.PeopleDirectory.Persistence/Migrations/20250527161934_InitialSchema.Designer.cs
generated
Normal file
457
Sufi.Demo.PeopleDirectory.Persistence/Migrations/20250527161934_InitialSchema.Designer.cs
generated
Normal file
@@ -0,0 +1,457 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Contexts;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20250527161934_InitialSchema")]
|
||||
partial class InitialSchema
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserClaims", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserLogins", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("UserRoles", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Domain.Entities.Misc.Contact", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Hobby")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("SkillSets")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Contacts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Domain.Entities.Misc.ServerInfo", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ServerInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Audit.Audit", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AffectedColumns")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("DateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NewValues")
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("OldValues")
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("PrimaryKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("TableName")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AuditTrails", "Audit");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("Roles", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRoleClaim", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AppRoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Group")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppRoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("RoleClaims", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("Users", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRoleClaim", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", null)
|
||||
.WithMany("RoleClaims")
|
||||
.HasForeignKey("AppRoleId");
|
||||
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", b =>
|
||||
{
|
||||
b.Navigation("RoleClaims");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialSchema : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "Audit");
|
||||
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "Identity");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AuditTrails",
|
||||
schema: "Audit",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
Type = table.Column<string>(type: "character varying(20)", nullable: false),
|
||||
TableName = table.Column<string>(type: "character varying(50)", nullable: false),
|
||||
DateTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
OldValues = table.Column<string>(type: "character varying(255)", nullable: true),
|
||||
NewValues = table.Column<string>(type: "character varying(255)", nullable: true),
|
||||
AffectedColumns = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
PrimaryKey = table.Column<string>(type: "character varying(100)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AuditTrails", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contacts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserName = table.Column<string>(type: "character varying(50)", nullable: false),
|
||||
Phone = table.Column<string>(type: "character varying(20)", nullable: false),
|
||||
Email = table.Column<string>(type: "character varying(100)", nullable: false),
|
||||
SkillSets = table.Column<string>(type: "character varying(255)", nullable: false),
|
||||
Hobby = table.Column<string>(type: "character varying(255)", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
CreatedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
LastModifiedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contacts", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Roles",
|
||||
schema: "Identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
CreatedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
LastModifiedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Roles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServerInfos",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
Value = table.Column<string>(type: "character varying(255)", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
CreatedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
LastModifiedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServerInfos", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
schema: "Identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "text", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DeletedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
CreatedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
LastModifiedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RoleClaims",
|
||||
schema: "Identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Description = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
Group = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
CreatedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "character varying(100)", nullable: true),
|
||||
LastModifiedOn = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
AppRoleId = table.Column<string>(type: "text", nullable: true),
|
||||
RoleId = table.Column<string>(type: "text", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RoleClaims_Roles_AppRoleId",
|
||||
column: x => x.AppRoleId,
|
||||
principalSchema: "Identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_RoleClaims_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalSchema: "Identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserClaims",
|
||||
schema: "Identity",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserClaims_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "Identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserLogins",
|
||||
schema: "Identity",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
|
||||
UserId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserLogins_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "Identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRoles",
|
||||
schema: "Identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
RoleId = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalSchema: "Identity",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoles_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "Identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserTokens",
|
||||
schema: "Identity",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "text", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Value = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_UserTokens_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "Identity",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RoleClaims_AppRoleId",
|
||||
schema: "Identity",
|
||||
table: "RoleClaims",
|
||||
column: "AppRoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RoleClaims_RoleId",
|
||||
schema: "Identity",
|
||||
table: "RoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
schema: "Identity",
|
||||
table: "Roles",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserClaims_UserId",
|
||||
schema: "Identity",
|
||||
table: "UserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserLogins_UserId",
|
||||
schema: "Identity",
|
||||
table: "UserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoles_RoleId",
|
||||
schema: "Identity",
|
||||
table: "UserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
schema: "Identity",
|
||||
table: "Users",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
schema: "Identity",
|
||||
table: "Users",
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AuditTrails",
|
||||
schema: "Audit");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contacts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RoleClaims",
|
||||
schema: "Identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServerInfos");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserClaims",
|
||||
schema: "Identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserLogins",
|
||||
schema: "Identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRoles",
|
||||
schema: "Identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserTokens",
|
||||
schema: "Identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Roles",
|
||||
schema: "Identity");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users",
|
||||
schema: "Identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Contexts;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserClaims", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserLogins", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("UserRoles", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Domain.Entities.Misc.Contact", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Hobby")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("SkillSets")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Contacts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Domain.Entities.Misc.ServerInfo", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ServerInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Audit.Audit", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AffectedColumns")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("DateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NewValues")
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("OldValues")
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("PrimaryKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("TableName")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AuditTrails", "Audit");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("Roles", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRoleClaim", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AppRoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Group")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppRoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("RoleClaims", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("LastModifiedOn")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("Users", "Identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRoleClaim", b =>
|
||||
{
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", null)
|
||||
.WithMany("RoleClaims")
|
||||
.HasForeignKey("AppRoleId");
|
||||
|
||||
b.HasOne("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Sufi.Demo.PeopleDirectory.Infrastructure.Models.Identity.AppRole", b =>
|
||||
{
|
||||
b.Navigation("RoleClaims");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Sufi.Demo.PeopleDirectory.Persistence/Models/Audit/Audit.cs
Normal file
25
Sufi.Demo.PeopleDirectory.Persistence/Models/Audit/Audit.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Models.Audit
|
||||
{
|
||||
public class Audit : IEntity<int>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[Column(TypeName = "character varying(100)")]
|
||||
public string? UserId { get; set; }
|
||||
[Column(TypeName = "character varying(20)")]
|
||||
public string Type { get; set; } = null!;
|
||||
[Column(TypeName = "character varying(50)")]
|
||||
public string TableName { get; set; } = null!;
|
||||
public DateTime DateTime { get; set; }
|
||||
[Column(TypeName = "character varying(255)")]
|
||||
public string? OldValues { get; set; }
|
||||
[Column(TypeName = "character varying(255)")]
|
||||
public string? NewValues { get; set; }
|
||||
[Column(TypeName = "character varying(100)")]
|
||||
public string? AffectedColumns { get; set; }
|
||||
[Column(TypeName = "character varying(100)")]
|
||||
public string PrimaryKey { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Enums;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Models.Audit
|
||||
{
|
||||
public class AuditEntry(EntityEntry entry)
|
||||
{
|
||||
public EntityEntry Entry { get; } = entry;
|
||||
public string? UserId { get; set; }
|
||||
public string TableName { get; set; } = null!;
|
||||
public Dictionary<string, object?> KeyValues { get; } = new();
|
||||
public Dictionary<string, object?> OldValues { get; } = new();
|
||||
public Dictionary<string, object?> NewValues { get; } = new();
|
||||
public List<PropertyEntry> TemporaryProperties { get; } = new();
|
||||
public AuditType AuditType { get; set; }
|
||||
public List<string> ChangedColumns { get; } = new();
|
||||
public bool HasTemporaryProperties => TemporaryProperties.Any();
|
||||
|
||||
public Audit ToAudit()
|
||||
{
|
||||
var audit = new Audit
|
||||
{
|
||||
UserId = UserId,
|
||||
Type = AuditType.ToString(),
|
||||
TableName = TableName,
|
||||
DateTime = DateTime.UtcNow,
|
||||
PrimaryKey = JsonSerializer.Serialize(KeyValues),
|
||||
OldValues = OldValues.Count == 0 ? null : JsonSerializer.Serialize(OldValues),
|
||||
NewValues = NewValues.Count == 0 ? null : JsonSerializer.Serialize(NewValues),
|
||||
AffectedColumns = ChangedColumns.Count == 0 ? null : JsonSerializer.Serialize(ChangedColumns)
|
||||
};
|
||||
return audit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Models.Identity
|
||||
{
|
||||
public class AppRole : IdentityRole, IAuditableEntity<string>
|
||||
{
|
||||
[Column(TypeName = "character varying(100)")]
|
||||
public string? Description { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public DateTime CreatedOn { get; set; }
|
||||
public string? LastModifiedBy { get; set; }
|
||||
public DateTime? LastModifiedOn { get; set; }
|
||||
public virtual ICollection<AppRoleClaim> RoleClaims { get; set; }
|
||||
|
||||
public AppRole() : base()
|
||||
{
|
||||
RoleClaims = new HashSet<AppRoleClaim>();
|
||||
}
|
||||
|
||||
public AppRole(string roleName, string? description = null) : base(roleName)
|
||||
{
|
||||
RoleClaims = new HashSet<AppRoleClaim>();
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Models.Identity
|
||||
{
|
||||
public class AppRoleClaim : IdentityRoleClaim<string>, IAuditableEntity<int>
|
||||
{
|
||||
[Column(TypeName = "character varying(100)")]
|
||||
public string? Description { get; set; }
|
||||
[Column(TypeName = "character varying(100)")]
|
||||
public string? Group { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public DateTime CreatedOn { get; set; }
|
||||
public string? LastModifiedBy { get; set; }
|
||||
public DateTime? LastModifiedOn { get; set; }
|
||||
|
||||
public AppRoleClaim() : base() { }
|
||||
|
||||
public AppRoleClaim(string? description = null, string? group = null) : base()
|
||||
{
|
||||
Description = description;
|
||||
Group = group;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Models.Identity
|
||||
{
|
||||
public class AppUser : IdentityUser<string>, IAuditableEntity<string>
|
||||
{
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime? DeletedOn { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public DateTime CreatedOn { get; set; }
|
||||
public string? LastModifiedBy { get; set; }
|
||||
public DateTime? LastModifiedOn { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Contexts;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Repositories
|
||||
{
|
||||
public class AsyncRepository<T, TId>(
|
||||
ApplicationDbContext dbContext
|
||||
) : IAsyncRepository<T, TId> where T : AuditableEntity<TId>
|
||||
{
|
||||
public IQueryable<T> Entities => dbContext.Set<T>();
|
||||
|
||||
public async Task<T> AddAsync(T entity)
|
||||
{
|
||||
await dbContext.Set<T>().AddAsync(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync() => await dbContext.Set<T>().CountAsync();
|
||||
|
||||
public Task DeleteAsync(T entity)
|
||||
{
|
||||
dbContext.Set<T>().Remove(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteByIdAsync(TId id)
|
||||
{
|
||||
var rowsAffected = await dbContext.Set<T>()
|
||||
.Where(e => e.Id != null && e.Id.Equals(id))
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
return rowsAffected;
|
||||
}
|
||||
|
||||
public async Task<List<T>> GetAllAsync()
|
||||
{
|
||||
return await dbContext
|
||||
.Set<T>()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<T?> GetByIdAsync(TId id)
|
||||
{
|
||||
return await dbContext.Set<T>().FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<List<T>> GetPagedResponseAsync(int pageNumber, int pageSize)
|
||||
{
|
||||
return await dbContext
|
||||
.Set<T>()
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task UpdateAsync(T entity)
|
||||
{
|
||||
T? exist = dbContext.Set<T>().Find(entity.Id);
|
||||
if (exist != null)
|
||||
{
|
||||
dbContext.Entry<T>(exist).CurrentValues.SetValues(entity);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Common;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Contexts;
|
||||
using System.Collections;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Persistence.Repositories
|
||||
{
|
||||
public class UnitOfWork<TId>(
|
||||
ApplicationDbContext dbContext
|
||||
) : IUnitOfWork<TId>
|
||||
{
|
||||
private bool disposed;
|
||||
private Hashtable? _repositories;
|
||||
|
||||
public IAsyncRepository<TEntity, TId> Repository<TEntity>() where TEntity : AuditableEntity<TId>
|
||||
{
|
||||
_repositories ??= [];
|
||||
|
||||
var type = typeof(TEntity).Name;
|
||||
|
||||
if (!_repositories.ContainsKey(type))
|
||||
{
|
||||
var repositoryType = typeof(AsyncRepository<,>);
|
||||
|
||||
var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity), typeof(TId)), dbContext);
|
||||
|
||||
_repositories.Add(type, repositoryInstance);
|
||||
}
|
||||
|
||||
return (IAsyncRepository<TEntity, TId>)_repositories[type]!;
|
||||
}
|
||||
|
||||
public async Task<int> Commit(CancellationToken cancellationToken)
|
||||
{
|
||||
return await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> CommitAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys)
|
||||
{
|
||||
var result = await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task Rollback()
|
||||
{
|
||||
dbContext.ChangeTracker.Entries().ToList().ForEach(x => x.Reload());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
//dispose managed resources
|
||||
dbContext.Dispose();
|
||||
}
|
||||
}
|
||||
//dispose unmanaged resources
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.21" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Application\Sufi.Demo.PeopleDirectory.Application.csproj" />
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Domain\Sufi.Demo.PeopleDirectory.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
using Sufi.Demo.PeopleDirectory.Shared.Wrapper;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Shared.Extensions
|
||||
{
|
||||
public static class ResultExtensions
|
||||
{
|
||||
public static async Task<IResult<T>> ToResult<T>(this HttpResponseMessage response) where T : notnull
|
||||
{
|
||||
var responseAsString = await response.Content.ReadAsStringAsync();
|
||||
var responseObject = JsonSerializer.Deserialize<Result<T>>(responseAsString, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReferenceHandler = ReferenceHandler.Preserve
|
||||
});
|
||||
return responseObject!;
|
||||
}
|
||||
|
||||
public static async Task<IResult> ToResult(this HttpResponseMessage response)
|
||||
{
|
||||
var responseAsString = await response.Content.ReadAsStringAsync();
|
||||
var responseObject = JsonSerializer.Deserialize<Result>(responseAsString, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReferenceHandler = ReferenceHandler.Preserve
|
||||
});
|
||||
return responseObject!;
|
||||
}
|
||||
|
||||
public static async Task<PaginatedResult<T>> ToPaginatedResult<T>(this HttpResponseMessage response) where T : notnull
|
||||
{
|
||||
var responseAsString = await response.Content.ReadAsStringAsync();
|
||||
var responseObject = JsonSerializer.Deserialize<PaginatedResult<T>>(responseAsString, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
return responseObject!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
16
Sufi.Demo.PeopleDirectory.Shared/Wrapper/IResult.cs
Normal file
16
Sufi.Demo.PeopleDirectory.Shared/Wrapper/IResult.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Shared.Wrapper
|
||||
{
|
||||
public interface IResult
|
||||
{
|
||||
List<string> Messages { get; set; }
|
||||
|
||||
bool Succeeded { get; set; }
|
||||
}
|
||||
|
||||
public interface IResult<out T> : IResult
|
||||
{
|
||||
T Data { get; }
|
||||
}
|
||||
}
|
||||
47
Sufi.Demo.PeopleDirectory.Shared/Wrapper/PaginatedResult.cs
Normal file
47
Sufi.Demo.PeopleDirectory.Shared/Wrapper/PaginatedResult.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Shared.Wrapper
|
||||
{
|
||||
public class PaginatedResult<T> : Result where T : notnull
|
||||
{
|
||||
public PaginatedResult(List<T> data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public List<T> Data { get; set; }
|
||||
|
||||
internal PaginatedResult(bool succeeded, List<T> data = default!, List<string>? messages = null, int count = 0, int page = 1, int pageSize = 10)
|
||||
{
|
||||
Data = data;
|
||||
CurrentPage = page;
|
||||
Succeeded = succeeded;
|
||||
PageSize = pageSize;
|
||||
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
|
||||
TotalCount = count;
|
||||
Messages = messages ?? [];
|
||||
}
|
||||
|
||||
public static PaginatedResult<T> Failure(List<string> messages)
|
||||
{
|
||||
return new PaginatedResult<T>(false, default!, messages);
|
||||
}
|
||||
|
||||
public static PaginatedResult<T> Success(List<T> data, int count, int page, int pageSize)
|
||||
{
|
||||
return new PaginatedResult<T>(true, data, null, count, page, pageSize);
|
||||
}
|
||||
|
||||
public int CurrentPage { get; set; }
|
||||
|
||||
public int TotalPages { get; set; }
|
||||
|
||||
public int TotalCount { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
|
||||
public bool HasPreviousPage => CurrentPage > 1;
|
||||
|
||||
public bool HasNextPage => CurrentPage < TotalPages;
|
||||
}
|
||||
}
|
||||
150
Sufi.Demo.PeopleDirectory.Shared/Wrapper/Result.cs
Normal file
150
Sufi.Demo.PeopleDirectory.Shared/Wrapper/Result.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.Shared.Wrapper
|
||||
{
|
||||
public class Result : IResult
|
||||
{
|
||||
public Result()
|
||||
{
|
||||
}
|
||||
|
||||
public List<string> Messages { get; set; } = [];
|
||||
|
||||
public bool Succeeded { get; set; }
|
||||
|
||||
public static IResult Fail()
|
||||
{
|
||||
return new Result { Succeeded = false };
|
||||
}
|
||||
|
||||
public static IResult Fail(string message)
|
||||
{
|
||||
return new Result { Succeeded = false, Messages = [message] };
|
||||
}
|
||||
|
||||
public static IResult Fail(List<string> messages)
|
||||
{
|
||||
return new Result { Succeeded = false, Messages = messages };
|
||||
}
|
||||
|
||||
public static Task<IResult> FailAsync()
|
||||
{
|
||||
return Task.FromResult(Fail());
|
||||
}
|
||||
|
||||
public static Task<IResult> FailAsync(string message)
|
||||
{
|
||||
return Task.FromResult(Fail(message));
|
||||
}
|
||||
|
||||
public static Task<IResult> FailAsync(List<string> messages)
|
||||
{
|
||||
return Task.FromResult(Fail(messages));
|
||||
}
|
||||
|
||||
public static IResult Success()
|
||||
{
|
||||
return new Result { Succeeded = true };
|
||||
}
|
||||
|
||||
public static IResult Success(string message)
|
||||
{
|
||||
return new Result { Succeeded = true, Messages = [message] };
|
||||
}
|
||||
|
||||
public static Task<IResult> SuccessAsync()
|
||||
{
|
||||
return Task.FromResult(Success());
|
||||
}
|
||||
|
||||
public static Task<IResult> SuccessAsync(string message)
|
||||
{
|
||||
return Task.FromResult(Success(message));
|
||||
}
|
||||
}
|
||||
|
||||
public class Result<T> : Result, IResult<T> where T : notnull
|
||||
{
|
||||
public Result()
|
||||
{
|
||||
}
|
||||
|
||||
public T Data { get; set; } = default!;
|
||||
|
||||
public new static Result<T> Fail()
|
||||
{
|
||||
return new Result<T> { Succeeded = false };
|
||||
}
|
||||
|
||||
public new static Result<T> Fail(string message)
|
||||
{
|
||||
return new Result<T> { Succeeded = false, Messages = [message] };
|
||||
}
|
||||
|
||||
public new static Result<T> Fail(List<string> messages)
|
||||
{
|
||||
return new Result<T> { Succeeded = false, Messages = messages };
|
||||
}
|
||||
|
||||
public new static Task<Result<T>> FailAsync()
|
||||
{
|
||||
return Task.FromResult(Fail());
|
||||
}
|
||||
|
||||
public new static Task<Result<T>> FailAsync(string message)
|
||||
{
|
||||
return Task.FromResult(Fail(message));
|
||||
}
|
||||
|
||||
public new static Task<Result<T>> FailAsync(List<string> messages)
|
||||
{
|
||||
return Task.FromResult(Fail(messages));
|
||||
}
|
||||
|
||||
public new static Result<T> Success()
|
||||
{
|
||||
return new Result<T> { Succeeded = true };
|
||||
}
|
||||
|
||||
public new static Result<T> Success(string message)
|
||||
{
|
||||
return new Result<T> { Succeeded = true, Messages = [message] };
|
||||
}
|
||||
|
||||
public static Result<T> Success(T data)
|
||||
{
|
||||
return new Result<T> { Succeeded = true, Data = data };
|
||||
}
|
||||
|
||||
public static Result<T> Success(T data, string message)
|
||||
{
|
||||
return new Result<T> { Succeeded = true, Data = data, Messages = [message] };
|
||||
}
|
||||
|
||||
public static Result<T> Success(T data, List<string> messages)
|
||||
{
|
||||
return new Result<T> { Succeeded = true, Data = data, Messages = messages };
|
||||
}
|
||||
|
||||
public new static Task<Result<T>> SuccessAsync()
|
||||
{
|
||||
return Task.FromResult(Success());
|
||||
}
|
||||
|
||||
public new static Task<Result<T>> SuccessAsync(string message)
|
||||
{
|
||||
return Task.FromResult(Success(message));
|
||||
}
|
||||
|
||||
public static Task<Result<T>> SuccessAsync(T data)
|
||||
{
|
||||
return Task.FromResult(Success(data));
|
||||
}
|
||||
|
||||
public static Task<Result<T>> SuccessAsync(T data, string message)
|
||||
{
|
||||
return Task.FromResult(Success(data, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UnitTests.Contacts
|
||||
{
|
||||
public class AddEditContactCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IMapper> _mapperMock;
|
||||
private readonly Mock<IUnitOfWork<int>> _unitOfWorkMock;
|
||||
private readonly Mock<ILogger<AddEditContactCommandHandler>> _loggerMock = new();
|
||||
private readonly Mock<IAppCache> _appCacheMock = new();
|
||||
private readonly AddEditContactCommandHandler _handler;
|
||||
|
||||
public AddEditContactCommandHandlerTests()
|
||||
{
|
||||
_mapperMock = new Mock<IMapper>();
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork<int>>();
|
||||
_handler = new AddEditContactCommandHandler(_mapperMock.Object, _unitOfWorkMock.Object, _loggerMock.Object, _appCacheMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldAddNewContact_WhenMaxCountNotExceeded()
|
||||
{
|
||||
// Arrange
|
||||
var command = new AddEditContactCommand
|
||||
{
|
||||
Id = 0,
|
||||
UserName = "JohnDoe",
|
||||
Email = "john@example.com",
|
||||
Phone = "1234567890",
|
||||
SkillSets = "C#, SQL",
|
||||
Hobby = "Reading"
|
||||
};
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().CountAsync()).ReturnsAsync(50);
|
||||
_mapperMock.Setup(m => m.Map<Contact>(command)).Returns(new Contact());
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().AddAsync(It.IsAny<Contact>())).Returns(Task.FromResult(new Contact()));
|
||||
_unitOfWorkMock.Setup(u => u.Commit(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.Equal("New contact saved.", result.Messages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldFailToAddNewContact_WhenMaxCountExceeded()
|
||||
{
|
||||
// Arrange
|
||||
var command = new AddEditContactCommand { Id = 0 };
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().CountAsync()).ReturnsAsync(101);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Equal("Max item count reached. Please delete some first.", result.Messages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldUpdateContact_WhenContactExists()
|
||||
{
|
||||
// Arrange
|
||||
var command = new AddEditContactCommand
|
||||
{
|
||||
Id = 1,
|
||||
UserName = "JaneDoe",
|
||||
Email = "jane@example.com",
|
||||
Phone = "0987654321",
|
||||
SkillSets = "Java, Python",
|
||||
Hobby = "Traveling"
|
||||
};
|
||||
var existingContact = new Contact { Id = 1 };
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(1)).ReturnsAsync(existingContact);
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().UpdateAsync(existingContact)).Returns(Task.CompletedTask);
|
||||
_unitOfWorkMock.Setup(u => u.Commit(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.Equal("Contact updated.", result.Messages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldFailToUpdateContact_WhenContactDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var command = new AddEditContactCommand { Id = 1 };
|
||||
Contact? existingContact = null;
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(1)).ReturnsAsync(existingContact);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Equal("Contact not found!", result.Messages[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UnitTests.Contacts
|
||||
{
|
||||
public class DeleteContactCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<IUnitOfWork<int>> _unitOfWorkMock;
|
||||
private readonly Mock<ILogger<DeleteContactCommandHandler>> _loggerMock = new();
|
||||
private readonly Mock<IAppCache> _appCacheMock = new();
|
||||
private readonly DeleteContactCommandHandler _handler;
|
||||
|
||||
public DeleteContactCommandHandlerTests()
|
||||
{
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork<int>>();
|
||||
_handler = new DeleteContactCommandHandler(_unitOfWorkMock.Object, _loggerMock.Object, _appCacheMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldDeleteContact_WhenContactExists()
|
||||
{
|
||||
// Arrange
|
||||
var command = new DeleteContactCommand { Id = 1 };
|
||||
var existingContact = new Contact { Id = 1 };
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(1)).ReturnsAsync(existingContact);
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().DeleteAsync(existingContact)).Returns(Task.CompletedTask);
|
||||
_unitOfWorkMock.Setup(u => u.Commit(It.IsAny<CancellationToken>())).ReturnsAsync(1);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.Empty(result.Messages); // Success messages are empty in this implementation
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShouldFailToDeleteContact_WhenContactDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var command = new DeleteContactCommand { Id = 1 };
|
||||
Contact? existingContact = null;
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(1)).ReturnsAsync(existingContact);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Equal("No data to delete.", result.Messages[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using AutoMapper;
|
||||
using Moq;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UnitTests.Contacts
|
||||
{
|
||||
public class GetContactByIdQueryHandlerTests
|
||||
{
|
||||
private readonly Mock<IUnitOfWork<int>> _unitOfWorkMock;
|
||||
private readonly Mock<IMapper> _mapperMock;
|
||||
private readonly Mock<IAppCache> _appCacheMock = new();
|
||||
private readonly GetContactByIdQueryHandler _handler;
|
||||
|
||||
public GetContactByIdQueryHandlerTests()
|
||||
{
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork<int>>();
|
||||
_mapperMock = new Mock<IMapper>();
|
||||
_handler = new GetContactByIdQueryHandler(_unitOfWorkMock.Object, _mapperMock.Object, _appCacheMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsMappedContact_WhenContactExists()
|
||||
{
|
||||
// Arrange
|
||||
var contact = new Contact
|
||||
{
|
||||
Id = 1,
|
||||
UserName = "User1",
|
||||
Phone = "123",
|
||||
Email = "user1@example.com",
|
||||
SkillSets = "C#",
|
||||
Hobby = "Reading"
|
||||
};
|
||||
var mappedContact = new GetContactByIdResponse
|
||||
{
|
||||
Id = 1,
|
||||
UserName = "User1",
|
||||
Phone = "123",
|
||||
Email = "user1@example.com",
|
||||
SkillSets = "C#",
|
||||
Hobby = "Reading"
|
||||
};
|
||||
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(1)).ReturnsAsync(contact);
|
||||
_mapperMock.Setup(m => m.Map<GetContactByIdResponse>(contact)).Returns(mappedContact);
|
||||
_appCacheMock.Setup(c => c.GetOrAddAsync(It.IsAny<string>(), It.IsAny<Func<CancellationToken, ValueTask<Contact>>>(), It.IsAny<IEnumerable<string>>(), It.IsAny<TimeSpan?>()))
|
||||
.ReturnsAsync(contact);
|
||||
|
||||
var query = new GetContactByIdQuery { Id = 1 };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.NotNull(result.Data);
|
||||
Assert.Equal(1, result.Data.Id);
|
||||
Assert.Equal("User1", result.Data.UserName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsNull_WhenContactDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
Contact? contact = null;
|
||||
GetContactByIdResponse? mappedContact = null;
|
||||
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(99)).ReturnsAsync(contact);
|
||||
_mapperMock.Setup(m => m.Map<GetContactByIdResponse>(contact)).Returns(mappedContact);
|
||||
|
||||
var query = new GetContactByIdQuery { Id = 99 };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.Null(result.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using AutoMapper;
|
||||
using Moq;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Repositories;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Contracts.Services;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById;
|
||||
using Sufi.Demo.PeopleDirectory.Domain.Entities.Misc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UnitTests.Contacts
|
||||
{
|
||||
public class GetContactsQueriesHandlerTests
|
||||
{
|
||||
private readonly Mock<IUnitOfWork<int>> _unitOfWorkMock;
|
||||
private readonly Mock<IMapper> _mapperMock;
|
||||
private readonly Mock<IAppCache> _appCacheMock = new();
|
||||
|
||||
public GetContactsQueriesHandlerTests()
|
||||
{
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork<int>>();
|
||||
_mapperMock = new Mock<IMapper>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllContactsQueryHandler_ReturnsMappedContacts()
|
||||
{
|
||||
// Arrange
|
||||
var contacts = new List<Contact>
|
||||
{
|
||||
new() { Id = 1, UserName = "User1", Phone = "123", Email = "user1@example.com", SkillSets = "C#", Hobby = "Reading" },
|
||||
new() { Id = 2, UserName = "User2", Phone = "456", Email = "user2@example.com", SkillSets = "Java", Hobby = "Swimming" }
|
||||
};
|
||||
var mappedContacts = new List<GetAllContactsResponse>
|
||||
{
|
||||
new() { Id = 1, UserName = "User1", Phone = "123", Email = "user1@example.com", SkillSets = "C#", Hobby = "Reading" },
|
||||
new() { Id = 2, UserName = "User2", Phone = "456", Email = "user2@example.com", SkillSets = "Java", Hobby = "Swimming" }
|
||||
};
|
||||
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetAllAsync()).ReturnsAsync(contacts);
|
||||
_mapperMock.Setup(m => m.Map<List<GetAllContactsResponse>>(contacts)).Returns(mappedContacts);
|
||||
_appCacheMock.Setup(c => c.GetOrAddAsync(It.IsAny<string>(), It.IsAny<Func<CancellationToken, ValueTask<List<Contact>>>>(), It.IsAny<IEnumerable<string>>(), It.IsAny<TimeSpan?>()))
|
||||
.ReturnsAsync(contacts);
|
||||
|
||||
var handler = new GetAllContactsQueryHandler(_unitOfWorkMock.Object, _mapperMock.Object, _appCacheMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(new GetAllContactsQuery(), CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.NotNull(result.Data);
|
||||
Assert.Equal(2, result.Data.Count);
|
||||
Assert.Equal("User1", result.Data[0].UserName);
|
||||
Assert.Equal("User2", result.Data[1].UserName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContactByIdQueryHandler_ReturnsMappedContact_WhenContactExists()
|
||||
{
|
||||
// Arrange
|
||||
var contact = new Contact { Id = 1, UserName = "User1", Phone = "123", Email = "user1@example.com", SkillSets = "C#", Hobby = "Reading" };
|
||||
var mappedContact = new GetContactByIdResponse { Id = 1, UserName = "User1", Phone = "123", Email = "user1@example.com", SkillSets = "C#", Hobby = "Reading" };
|
||||
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(1)).ReturnsAsync(contact);
|
||||
_mapperMock.Setup(m => m.Map<GetContactByIdResponse>(contact)).Returns(mappedContact);
|
||||
_appCacheMock.Setup(c => c.GetOrAddAsync(It.IsAny<string>(), It.IsAny<Func<CancellationToken, ValueTask<Contact>>>(), It.IsAny<IEnumerable<string>>(), It.IsAny<TimeSpan?>()))
|
||||
.ReturnsAsync(contact);
|
||||
|
||||
var handler = new GetContactByIdQueryHandler(_unitOfWorkMock.Object, _mapperMock.Object, _appCacheMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(new GetContactByIdQuery { Id = 1 }, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.NotNull(result.Data);
|
||||
Assert.Equal(1, result.Data.Id);
|
||||
Assert.Equal("User1", result.Data.UserName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetContactByIdQueryHandler_ReturnsNullData_WhenContactDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
Contact? contact = null;
|
||||
GetContactByIdResponse? mappedContact = null;
|
||||
|
||||
_unitOfWorkMock.Setup(u => u.Repository<Contact>().GetByIdAsync(99)).ReturnsAsync(contact);
|
||||
_mapperMock.Setup(m => m.Map<GetContactByIdResponse>(contact)).Returns(mappedContact);
|
||||
|
||||
var handler = new GetContactByIdQueryHandler(_unitOfWorkMock.Object, _mapperMock.Object, _appCacheMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await handler.Handle(new GetContactByIdQuery { Id = 99 }, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.Null(result.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Application\Sufi.Demo.PeopleDirectory.Application.csproj" />
|
||||
<ProjectReference Include="..\Sufi.Demo.PeopleDirectory.Shared\Sufi.Demo.PeopleDirectory.Shared.csproj" />
|
||||
<ProjectReference Include="..\ui\Sufi.Demo.PeopleDirectory.UI\Server\Sufi.Demo.PeopleDirectory.UI.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
101
Sufi.Demo.PeopleDirectory.sln
Normal file
101
Sufi.Demo.PeopleDirectory.sln
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.6.33829.357
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sufi.Demo.PeopleDirectory.UI.Server", "ui\Sufi.Demo.PeopleDirectory.UI\Server\Sufi.Demo.PeopleDirectory.UI.Server.csproj", "{86ADD71C-9EFE-4D80-98C6-AA9539E2CC68}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sufi.Demo.PeopleDirectory.UI.Client", "ui\Sufi.Demo.PeopleDirectory.UI\Client\Sufi.Demo.PeopleDirectory.UI.Client.csproj", "{39B057E5-B151-4C7F-B172-904227D6778E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{35940606-8CE4-421C-A0AA-0A829A7D44F6}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitea\workflows\build.yml = .gitea\workflows\build.yml
|
||||
.gitea\workflows\deploy.yml = .gitea\workflows\deploy.yml
|
||||
docker-compose.yml = docker-compose.yml
|
||||
Dockerfile = Dockerfile
|
||||
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{01F8753B-392E-48C7-A16A-327BBE2658ED}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{C7579627-572E-4682-9FA0-8FBEA29B6C1F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{662C8048-BE05-464E-8F6E-DFEC5BDA3038}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{F15D83A4-8080-4750-A025-A0151B8B3577}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sufi.Demo.PeopleDirectory.Domain", "Sufi.Demo.PeopleDirectory.Domain\Sufi.Demo.PeopleDirectory.Domain.csproj", "{3DD52A3E-9BA6-4D71-A0EF-A0E7DE5C98CE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sufi.Demo.PeopleDirectory.Persistence", "Sufi.Demo.PeopleDirectory.Persistence\Sufi.Demo.PeopleDirectory.Persistence.csproj", "{78D653E8-7B22-4BB1-8AD7-EE16CB69A21D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sufi.Demo.PeopleDirectory.Application", "Sufi.Demo.PeopleDirectory.Application\Sufi.Demo.PeopleDirectory.Application.csproj", "{3A107445-083F-441D-8C1E-800E27156084}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sufi.Demo.PeopleDirectory.Shared", "Sufi.Demo.PeopleDirectory.Shared\Sufi.Demo.PeopleDirectory.Shared.csproj", "{8ECB01B5-9C96-4C41-9D61-B286B470D1C5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sufi.Demo.PeopleDirectory.UnitTests", "Sufi.Demo.PeopleDirectory.UnitTests\Sufi.Demo.PeopleDirectory.UnitTests.csproj", "{D278DEB8-EE96-453E-AD1C-BF0E08F1A70A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sufi.Demo.PeopleDirectory.Infrastructure", "Sufi.Demo.PeopleDirectory.Infrastructure\Sufi.Demo.PeopleDirectory.Infrastructure.csproj", "{F3D2240B-A4F7-2F52-151C-941C2F32F0BA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{86ADD71C-9EFE-4D80-98C6-AA9539E2CC68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{86ADD71C-9EFE-4D80-98C6-AA9539E2CC68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{86ADD71C-9EFE-4D80-98C6-AA9539E2CC68}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{86ADD71C-9EFE-4D80-98C6-AA9539E2CC68}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{39B057E5-B151-4C7F-B172-904227D6778E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{39B057E5-B151-4C7F-B172-904227D6778E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{39B057E5-B151-4C7F-B172-904227D6778E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{39B057E5-B151-4C7F-B172-904227D6778E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3DD52A3E-9BA6-4D71-A0EF-A0E7DE5C98CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3DD52A3E-9BA6-4D71-A0EF-A0E7DE5C98CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3DD52A3E-9BA6-4D71-A0EF-A0E7DE5C98CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3DD52A3E-9BA6-4D71-A0EF-A0E7DE5C98CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{78D653E8-7B22-4BB1-8AD7-EE16CB69A21D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{78D653E8-7B22-4BB1-8AD7-EE16CB69A21D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{78D653E8-7B22-4BB1-8AD7-EE16CB69A21D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{78D653E8-7B22-4BB1-8AD7-EE16CB69A21D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3A107445-083F-441D-8C1E-800E27156084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3A107445-083F-441D-8C1E-800E27156084}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3A107445-083F-441D-8C1E-800E27156084}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3A107445-083F-441D-8C1E-800E27156084}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8ECB01B5-9C96-4C41-9D61-B286B470D1C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8ECB01B5-9C96-4C41-9D61-B286B470D1C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8ECB01B5-9C96-4C41-9D61-B286B470D1C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8ECB01B5-9C96-4C41-9D61-B286B470D1C5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D278DEB8-EE96-453E-AD1C-BF0E08F1A70A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D278DEB8-EE96-453E-AD1C-BF0E08F1A70A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D278DEB8-EE96-453E-AD1C-BF0E08F1A70A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D278DEB8-EE96-453E-AD1C-BF0E08F1A70A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F3D2240B-A4F7-2F52-151C-941C2F32F0BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F3D2240B-A4F7-2F52-151C-941C2F32F0BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F3D2240B-A4F7-2F52-151C-941C2F32F0BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F3D2240B-A4F7-2F52-151C-941C2F32F0BA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{86ADD71C-9EFE-4D80-98C6-AA9539E2CC68} = {F15D83A4-8080-4750-A025-A0151B8B3577}
|
||||
{39B057E5-B151-4C7F-B172-904227D6778E} = {F15D83A4-8080-4750-A025-A0151B8B3577}
|
||||
{C7579627-572E-4682-9FA0-8FBEA29B6C1F} = {01F8753B-392E-48C7-A16A-327BBE2658ED}
|
||||
{662C8048-BE05-464E-8F6E-DFEC5BDA3038} = {01F8753B-392E-48C7-A16A-327BBE2658ED}
|
||||
{F15D83A4-8080-4750-A025-A0151B8B3577} = {01F8753B-392E-48C7-A16A-327BBE2658ED}
|
||||
{3DD52A3E-9BA6-4D71-A0EF-A0E7DE5C98CE} = {C7579627-572E-4682-9FA0-8FBEA29B6C1F}
|
||||
{78D653E8-7B22-4BB1-8AD7-EE16CB69A21D} = {662C8048-BE05-464E-8F6E-DFEC5BDA3038}
|
||||
{3A107445-083F-441D-8C1E-800E27156084} = {C7579627-572E-4682-9FA0-8FBEA29B6C1F}
|
||||
{8ECB01B5-9C96-4C41-9D61-B286B470D1C5} = {C7579627-572E-4682-9FA0-8FBEA29B6C1F}
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {01F8753B-392E-48C7-A16A-327BBE2658ED}
|
||||
{D278DEB8-EE96-453E-AD1C-BF0E08F1A70A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{F3D2240B-A4F7-2F52-151C-941C2F32F0BA} = {662C8048-BE05-464E-8F6E-DFEC5BDA3038}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {04BF9A3C-F187-44A3-9F34-8BE6D3286782}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
demo-api:
|
||||
container_name: demo-contact-api
|
||||
image: sufiaziz/demo-contact:latest
|
||||
ports:
|
||||
- 5001:8080
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ASPNETCORE_URLS: http://0.0.0.0:8080
|
||||
ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
|
||||
ConnectionStrings__DefaultConnectionString: #ConnectionString#
|
||||
LuckyPennyLicenseKey: #LuckyPennyLicenseKey#
|
||||
Serilog__WriteTo__1__Args__serverUrl: #SEQ_URL#
|
||||
Serilog__WriteTo__1__Args__apiKey: #SEQ_APIKEY#
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- postgres-net
|
||||
- seq-net
|
||||
|
||||
networks:
|
||||
postgres-net:
|
||||
name: postgres-net
|
||||
external: true
|
||||
seq-net:
|
||||
name: seq-net
|
||||
external: true
|
||||
12
ui/Sufi.Demo.PeopleDirectory.UI/Client/App.razor
Normal file
12
ui/Sufi.Demo.PeopleDirectory.UI/Client/App.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
210
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Contacts.razor
Normal file
210
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Contacts.razor
Normal file
@@ -0,0 +1,210 @@
|
||||
@page "/contacts"
|
||||
|
||||
@inject HttpClient Http
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject BoomerangService Boomerang
|
||||
|
||||
<PageTitle>Contact List</PageTitle>
|
||||
|
||||
<MudGrid Class="py-4">
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.h3">Contact List</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.body1">Behold! Below is the list of all registered users in this application.</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Error">(All data will be deleted for every 10 minutes)</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" Class="d-flex gap-4">
|
||||
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="OnButtonCreateClicked">Create</MudButton>
|
||||
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Refresh" OnClick="OnButtonRefreshClicked">Refresh</MudButton>
|
||||
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
@if (contacts != null && contacts.Length > 0)
|
||||
{
|
||||
<MudTable Items="@contacts" Hover Breakpoint="Breakpoint.Sm" Loading="@loading" LoadingProgressColor="Color.Info" Class="py-3">
|
||||
<HeaderContent>
|
||||
<MudTh>Id</MudTh>
|
||||
<MudTh>Username</MudTh>
|
||||
<MudTh>Phone</MudTh>
|
||||
<MudTh>Email</MudTh>
|
||||
<MudTh>Skill Sets</MudTh>
|
||||
<MudTh>Hobby</MudTh>
|
||||
<MudTh>Action</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Id">@context.Id</MudTd>
|
||||
<MudTd DataLabel="Username">@context.UserName</MudTd>
|
||||
<MudTd DataLabel="Phone">@context.Phone</MudTd>
|
||||
<MudTd DataLabel="Email">@context.Email</MudTd>
|
||||
<MudTd DataLabel="Skill Sets">@context.SkillSets</MudTd>
|
||||
<MudTd DataLabel="Hobby">@context.Hobby</MudTd>
|
||||
<MudTd>
|
||||
<MudTooltip Text="Edit" Placement="Placement.Top" Arrow>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" OnClick="() => OnEditContactClicked(context.Id)" Color="Color.Warning" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Delete" Placement="Placement.Top" Arrow>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" OnClick="() => OnDeleteContactClicked(context.Id)" Color="Color.Error" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager />
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.h5">No item to display. Click on the 'Create' button to add.</MudText>
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudOverlay Visible="loading" DarkBackground ZIndex="9999">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate />
|
||||
</MudOverlay>
|
||||
|
||||
@code {
|
||||
private GetAllContactsResponse[]? contacts;
|
||||
private bool loading;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadTableAsync();
|
||||
|
||||
await Boomerang.AddVariableAsync("PageName", "Contacts");
|
||||
await Boomerang.SendBeaconAsync();
|
||||
}
|
||||
|
||||
private async Task LoadTableAsync()
|
||||
{
|
||||
loading = true;
|
||||
|
||||
var response = await Http.GetAsync("api/v1/contacts");
|
||||
var result = await response.ToResult<List<GetAllContactsResponse>>();
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
contacts = result.Data.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
contacts = null;
|
||||
|
||||
foreach (var message in result.Messages)
|
||||
{
|
||||
Snackbar.Add(message, Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private async Task OnButtonRefreshClicked()
|
||||
{
|
||||
await LoadTableAsync();
|
||||
}
|
||||
|
||||
private async Task OnButtonCreateClicked()
|
||||
{
|
||||
var dialogOptions = new DialogOptions { BackdropClick = false, CloseButton = true };
|
||||
var dialog = await DialogService.ShowAsync<CreateContactDialog>("Create New Contact", dialogOptions);
|
||||
var dialogResult = await dialog.Result;
|
||||
|
||||
if (dialogResult!.Canceled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var contactData = (AddEditContactCommand)dialogResult.Data!;
|
||||
var response = await Http.PostAsJsonAsync("api/v1/contacts", contactData);
|
||||
var result = await response.ToResult<int>();
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Snackbar.Add(result.Messages[0], Severity.Success);
|
||||
await LoadTableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var message in result.Messages)
|
||||
{
|
||||
Snackbar.Add(message, Severity.Error);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async Task OnEditContactClicked(int id)
|
||||
{
|
||||
var dialogParams = new DialogParameters { ["Id"] = id };
|
||||
var dialogOptions = new DialogOptions { BackdropClick = false, CloseButton = true };
|
||||
var dialog = await DialogService.ShowAsync<EditContactDialog>("Edit Contact", dialogParams, dialogOptions);
|
||||
var dialogResult = await dialog.Result;
|
||||
if (dialogResult!.Canceled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var contactData = (AddEditContactCommand)dialogResult.Data!;
|
||||
var response = await Http.PostAsJsonAsync("api/v1/contacts", contactData);
|
||||
var result = await response.ToResult<int>();
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Snackbar.Add(result.Messages[0], Severity.Success);
|
||||
await LoadTableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var message in result.Messages)
|
||||
{
|
||||
Snackbar.Add(message, Severity.Error);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async Task OnDeleteContactClicked(int id)
|
||||
{
|
||||
var dialogResult = await DialogService.ShowMessageBox("Delete Contact", "Confirm to delete the item? This action cannot be undone.", yesText: "Delete!", cancelText: "No");
|
||||
if (dialogResult == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var deleteResponse = await Http.DeleteAsync($"api/v1/contacts/{id}");
|
||||
if (deleteResponse.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await deleteResponse.ToResult();
|
||||
if (result!.Succeeded)
|
||||
{
|
||||
Snackbar.Add("Contact deleted successfully.", Severity.Success);
|
||||
await LoadTableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(result.Messages[0], Severity.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("An error has occurred.", Severity.Error);
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="form" Model="request" @bind-IsValid="success" @bind-Errors="errors">
|
||||
<MudTextField Label="Username" Required @bind-Value="request.UserName" />
|
||||
<MudTextField Label="Email" Required @bind-Value="request.Email" />
|
||||
<MudTextField Label="Phone" Required @bind-Value="request.Phone" MaxLength="20" />
|
||||
<MudTextField Label="Skill Sets" Required @bind-Value="request.SkillSets" />
|
||||
<MudTextField Label="Hobby" Required @bind-Value="request.Hobby" />
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton OnClick="Submit">Ok</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
private static readonly string[] SampleUserNames = new[]
|
||||
{
|
||||
"alex99",
|
||||
"samwise",
|
||||
"lunaStar",
|
||||
"maverick",
|
||||
"nova",
|
||||
"pixelPro",
|
||||
"kanu",
|
||||
"tay",
|
||||
"zorin",
|
||||
"echo"
|
||||
};
|
||||
private static readonly Random _rnd = new();
|
||||
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
private AddEditContactCommand request = new();
|
||||
private MudForm? form;
|
||||
private bool success;
|
||||
private string[] errors = { };
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
await form!.Validate();
|
||||
if (!success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MudDialog.Close(request);
|
||||
}
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
// Assign a random username from the in-memory list when the dialog opens
|
||||
request.UserName = SampleUserNames[_rnd.Next(SampleUserNames.Length)];
|
||||
return base.OnInitializedAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById
|
||||
|
||||
@inject HttpClient Http
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@if (showAlert)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@alertMessage</MudAlert>
|
||||
}
|
||||
<MudForm @ref="form" Model="request" @bind-IsValid="success" @bind-Errors="errors">
|
||||
<MudTextField Label="Username" Required="true" @bind-Value="request.UserName" />
|
||||
<MudTextField Label="Email" Required="true" @bind-Value="request.Email" />
|
||||
<MudTextField Label="Phone" Required="true" @bind-Value="request.Phone" />
|
||||
<MudTextField Label="Skill Sets" Required="true" @bind-Value="request.SkillSets" />
|
||||
<MudTextField Label="Hobby" Required="true" @bind-Value="request.Hobby" />
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton OnClick="Submit">Ok</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private AddEditContactCommand request = new();
|
||||
private MudForm? form;
|
||||
private bool success;
|
||||
private string[] errors = { };
|
||||
private bool showAlert;
|
||||
private string? alertMessage;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var response = await Http.GetAsync($"api/v1/contacts/{Id}");
|
||||
var result = await response.ToResult<GetContactByIdResponse>();
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var contact = result.Data;
|
||||
request.Id = Id;
|
||||
request.UserName = contact!.UserName;
|
||||
request.Email = contact.Email;
|
||||
request.Phone = contact.Phone;
|
||||
request.SkillSets = contact.SkillSets;
|
||||
request.Hobby = contact.Hobby;
|
||||
|
||||
showAlert = false;
|
||||
alertMessage = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
showAlert = true;
|
||||
alertMessage = string.Join(',', result.Messages);
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
await form!.Validate();
|
||||
if (!success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MudDialog!.Close(request);
|
||||
}
|
||||
}
|
||||
22
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Index.razor
Normal file
22
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Index.razor
Normal file
@@ -0,0 +1,22 @@
|
||||
@page "/"
|
||||
|
||||
@inject BoomerangService Boomerang
|
||||
|
||||
<PageTitle>Index - Demo App</PageTitle>
|
||||
|
||||
<MudGrid Class="py-4">
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.h3">Welcome to this demo app.</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.body1">Please view the contact list page.</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@code {
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await Boomerang.AddVariableAsync("PageName", "Index");
|
||||
await Boomerang.SendBeaconAsync();
|
||||
}
|
||||
}
|
||||
25
ui/Sufi.Demo.PeopleDirectory.UI/Client/Program.cs
Normal file
25
ui/Sufi.Demo.PeopleDirectory.UI/Client/Program.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using MudBlazor;
|
||||
using MudBlazor.Services;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Client;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Client.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
// MudBlazor
|
||||
builder.Services.AddMudServices(config =>
|
||||
{
|
||||
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.TopRight;
|
||||
config.SnackbarConfiguration.ClearAfterNavigation = false;
|
||||
config.SnackbarConfiguration.HideTransitionDuration = 200;
|
||||
config.SnackbarConfiguration.ShowTransitionDuration = 200;
|
||||
config.SnackbarConfiguration.NewestOnTop = true;
|
||||
});
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddScoped<BoomerangService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:50212",
|
||||
"sslPort": 44358
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Sufi.Demo.PeopleDirectory.UI": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7024;http://localhost:5243",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Client.Services
|
||||
{
|
||||
public class BoomerangService(
|
||||
IJSRuntime jsRuntime
|
||||
)
|
||||
{
|
||||
public async Task<bool> InitializeAsync(object config)
|
||||
{
|
||||
return await jsRuntime.InvokeAsync<bool>("boomerangHelper.init", config);
|
||||
}
|
||||
|
||||
public async Task AddVariableAsync(string key, string value)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("boomerangHelper.addVar", key, value);
|
||||
}
|
||||
|
||||
public async Task SendBeaconAsync()
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("boomerangHelper.sendBeacon");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider MaxWidth="MaxWidth.ExtraSmall" FullWidth="true" />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Color="Color.Info">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@(e => ToggleDrawer())" />
|
||||
<MudSpacer />
|
||||
</MudAppBar>
|
||||
<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Never">
|
||||
<SideNavMenu />
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.False">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
bool _drawerOpen = true;
|
||||
|
||||
void ToggleDrawer()
|
||||
{
|
||||
_drawerOpen = !_drawerOpen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row:not(.auth) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-row.auth {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<MudNavMenu Bordered>
|
||||
<MudText Typo="Typo.h6" Class="px-4 pt-3">Sufi Demo App</MudText>
|
||||
<MudText Typo="Typo.body2" Class="px-4 mud-text-secondary">For demo only</MudText>
|
||||
<MudDivider Class="my-2" />
|
||||
|
||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">Home</MudNavLink>
|
||||
|
||||
<MudNavLink Href="/contacts" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.List">Contact List</MudNavLink>
|
||||
<MudDivider Class="my-2" />
|
||||
|
||||
</MudNavMenu>
|
||||
@@ -0,0 +1,68 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.21" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.21" PrivateAssets="all" />
|
||||
<PackageReference Include="MudBlazor" Version="8.13.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Application\Sufi.Demo.PeopleDirectory.Application.csproj" />
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Shared\Sufi.Demo.PeopleDirectory.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Pages\Auth\" />
|
||||
</ItemGroup>
|
||||
|
||||
<UsingTask TaskName="ComputeHtmlTicks" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<Ticks ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
Ticks = DateTime.UtcNow.Ticks.ToString();
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<UsingTask TaskName="ReplaceTokenInFile" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<TemplateFile ParameterType="System.String" Required="true" />
|
||||
<OutputFile ParameterType="System.String" Required="true" />
|
||||
<Token ParameterType="System.String" Required="true" />
|
||||
<Replacement ParameterType="System.String" Required="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
var content = File.ReadAllText(TemplateFile);
|
||||
content = content.Replace(Token, Replacement);
|
||||
File.WriteAllText(OutputFile, content);
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<Target Name="InjectHtmlTicks" BeforeTargets="Build;Publish">
|
||||
<ComputeHtmlTicks>
|
||||
<Output TaskParameter="Ticks" PropertyName="HtmlTicks" />
|
||||
</ComputeHtmlTicks>
|
||||
|
||||
<Message Text="Computed Ticks: $(HtmlTicks)" Importance="Normal" />
|
||||
|
||||
<!-- write wwwroot/index.html from template -->
|
||||
<ReplaceTokenInFile TemplateFile="$(MSBuildProjectDirectory)\wwwroot\index.template.html" OutputFile="$(MSBuildProjectDirectory)\wwwroot\index.html" Token="__HTML_TICKS__" Replacement="$(HtmlTicks)" />
|
||||
</Target>
|
||||
</Project>
|
||||
17
ui/Sufi.Demo.PeopleDirectory.UI/Client/_Imports.razor
Normal file
17
ui/Sufi.Demo.PeopleDirectory.UI/Client/_Imports.razor
Normal file
@@ -0,0 +1,17 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client.Shared
|
||||
@using MudBlazor;
|
||||
@using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands
|
||||
@using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll
|
||||
@using Sufi.Demo.PeopleDirectory.Shared.Wrapper
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client.Pages.Dialogs
|
||||
@using Sufi.Demo.PeopleDirectory.Shared.Extensions
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client.Services
|
||||
62
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/css/app.css
Normal file
62
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/css/app.css
Normal file
@@ -0,0 +1,62 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/favicon.ico
Normal file
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/icon-192.png
Normal file
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
34
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/index.html
Normal file
34
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Sufi Demo App</title>
|
||||
<base href="/" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css?v=639044765520078561" rel="stylesheet" />
|
||||
<link href="css/app.css?v=639044765520078561" rel="stylesheet" />
|
||||
<link href="Sufi.Demo.PeopleDirectory.UI.Client.styles.css?v=639044765520078561" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js?v=639044765520078561"></script>
|
||||
<script src="js/boomerang.js"></script>
|
||||
<script src="js/boomerang-interop.js"></script>
|
||||
<script>
|
||||
BOOMR.init({
|
||||
beacon_url: "api/v1/beacon/"
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Sufi Demo App</title>
|
||||
<base href="/" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css?v=__HTML_TICKS__" rel="stylesheet" />
|
||||
<link href="css/app.css?v=__HTML_TICKS__" rel="stylesheet" />
|
||||
<link href="Sufi.Demo.PeopleDirectory.UI.Client.styles.css?v=__HTML_TICKS__" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js?v=__HTML_TICKS__"></script>
|
||||
<script src="js/boomerang.js"></script>
|
||||
<script src="js/boomerang-interop.js"></script>
|
||||
<script>
|
||||
BOOMR.init({
|
||||
beacon_url: "api/v1/beacon/"
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
window.boomerangHelper = {
|
||||
init: function (config) {
|
||||
if (window.BOOMR && window.BOOMR.init) {
|
||||
BOOMR.init(config);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
addVar: function (key, value) {
|
||||
if (window.BOOMR) {
|
||||
BOOMR.addVar(key, value);
|
||||
}
|
||||
},
|
||||
|
||||
sendBeacon: function () {
|
||||
if (window.BOOMR) {
|
||||
BOOMR.sendBeacon();
|
||||
}
|
||||
}
|
||||
};
|
||||
5360
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/js/boomerang.js
Normal file
5360
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/js/boomerang.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.10",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent the base controller class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public abstract class BaseApiController<T> : ControllerBase
|
||||
{
|
||||
private IMediator? _mediatorInstance;
|
||||
private ILogger<T>? _loggerInstance;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mediator for requests/responses.
|
||||
/// </summary>
|
||||
protected IMediator Mediator => _mediatorInstance ??= HttpContext.RequestServices.GetRequiredService<IMediator>();
|
||||
/// <summary>
|
||||
/// Gets the logger for current controller.
|
||||
/// </summary>
|
||||
protected ILogger<T> Logger => _loggerInstance ??= HttpContext.RequestServices.GetRequiredService<ILogger<T>>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v1
|
||||
{
|
||||
[ApiVersion(1.0)]
|
||||
public class BeaconController : BaseApiController<BeaconController>
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var beaconData = new Dictionary<string, string>();
|
||||
|
||||
if (Request.Method == "GET")
|
||||
{
|
||||
foreach (var param in Request.Query)
|
||||
{
|
||||
beaconData[param.Key] = param.Value.ToString();
|
||||
}
|
||||
}
|
||||
else if (Request.Method == "POST")
|
||||
{
|
||||
var contentType = Request.ContentType?.ToLower();
|
||||
|
||||
if (contentType?.Contains(MediaTypeNames.Application.Json) == true)
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var jsonData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(body)!;
|
||||
|
||||
foreach (var kvp in jsonData)
|
||||
{
|
||||
beaconData[kvp.Key] = kvp.Value.ToString();
|
||||
}
|
||||
}
|
||||
else if (contentType?.Contains(MediaTypeNames.Application.FormUrlEncoded) == true)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var kvp in form)
|
||||
{
|
||||
beaconData[kvp.Key] = kvp.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v1
|
||||
{
|
||||
/// <summary>
|
||||
/// A controller for manipulating contact data.
|
||||
/// </summary>
|
||||
[ApiVersion(1.0)]
|
||||
public class ContactsController : BaseApiController<ContactsController>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all contacts.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll() => Ok(await Mediator.Send(new GetAllContactsQuery()));
|
||||
|
||||
/// <summary>
|
||||
/// Get a contact by id.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(int id) => Ok(await Mediator.Send(new GetContactByIdQuery { Id = id }));
|
||||
|
||||
/// <summary>
|
||||
/// Create/Update a contact.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Post(AddEditContactCommand request) => Ok(await Mediator.Send(request));
|
||||
|
||||
/// <summary>
|
||||
/// Delete a contact.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete([Required] int id)
|
||||
{
|
||||
var command = new DeleteContactCommand { Id = id };
|
||||
return Ok(await Mediator.Send(command));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v1
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides API endpoints for infrastructure-related operations.
|
||||
/// </summary>
|
||||
/// <remarks>This controller is part of the infrastructure layer and includes endpoints for monitoring and diagnostics.</remarks>
|
||||
[ApiVersion(1.0)]
|
||||
public class InfraController(
|
||||
ILogger<InfraController> logger
|
||||
) : BaseApiController<InfraController>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulates a ping to check if the server is responsive.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Route("ping")]
|
||||
[HttpGet]
|
||||
public IActionResult Ping()
|
||||
{
|
||||
logger.LogInformation("InfraController.Ping method called.");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v2
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides API endpoints for infrastructure-related operations.
|
||||
/// </summary>
|
||||
/// <remarks>This controller is part of the infrastructure layer and includes endpoints for monitoring and diagnostics.</remarks>
|
||||
[ApiVersion(2.0)]
|
||||
public class InfraController : BaseApiController<InfraController>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulates a ping to check if the server is responsive.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Route("ping")]
|
||||
[HttpGet]
|
||||
public IActionResult Ping() => Ok("Server is OK");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension class.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
internal static void RegisterSwagger(this IServiceCollection services)
|
||||
{
|
||||
services.ConfigureOptions<ConfigureSwaggerGenOptions>();
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
//TODO - Lowercase Swagger Documents
|
||||
//c.DocumentFilter<LowercaseDocumentFilter>();
|
||||
//Refer - https://gist.github.com/rafalkasa/01d5e3b265e5aa075678e0adfd54e23f
|
||||
|
||||
// include all project's xml comments
|
||||
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (!assembly.IsDynamic)
|
||||
{
|
||||
var xmlFile = $"{assembly.GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(baseDirectory, xmlFile);
|
||||
if (File.Exists(xmlPath))
|
||||
{
|
||||
c.IncludeXmlComments(xmlPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.OperationFilter<SwaggerDefaultValues>();
|
||||
})
|
||||
.AddApiVersioning(config =>
|
||||
{
|
||||
config.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
config.AssumeDefaultVersionWhenUnspecified = true;
|
||||
config.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
})
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class ConfigureSwaggerGenOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
foreach (var description in provider.ApiVersionDescriptions)
|
||||
{
|
||||
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
|
||||
{
|
||||
var text = new StringBuilder("This application is for demo purposes by Sufi only.");
|
||||
var info = new OpenApiInfo
|
||||
{
|
||||
Title = "Sufi.Demo.App",
|
||||
Version = description.ApiVersion.ToString(),
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
Name = "MIT License",
|
||||
Url = new Uri("https://opensource.org/licenses/MIT")
|
||||
}
|
||||
};
|
||||
|
||||
if (description.IsDeprecated)
|
||||
{
|
||||
text.Append("This Api version has been deprecated.");
|
||||
}
|
||||
|
||||
if (description.SunsetPolicy is SunsetPolicy policy && policy.Date is DateTimeOffset when)
|
||||
{
|
||||
text.Append(" The Api will be sunset on ")
|
||||
.Append(when.Date.ToShortDateString())
|
||||
.Append('.');
|
||||
}
|
||||
|
||||
info.Description = text.ToString();
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SwaggerDefaultValues : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var apiDescription = context.ApiDescription;
|
||||
|
||||
operation.Deprecated |= apiDescription.IsDeprecated();
|
||||
|
||||
foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
|
||||
{
|
||||
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
|
||||
var response = operation.Responses[responseKey];
|
||||
|
||||
foreach (var contentType in response.Content.Keys)
|
||||
{
|
||||
if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
|
||||
{
|
||||
response.Content.Remove(contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operation.Parameters == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var parameter in operation.Parameters)
|
||||
{
|
||||
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
|
||||
|
||||
parameter.Description ??= description.ModelMetadata.Description;
|
||||
|
||||
if (parameter.Schema.Default == null &&
|
||||
description.DefaultValue != null &&
|
||||
description.DefaultValue is not DBNull &&
|
||||
description.ModelMetadata is ModelMetadata modelMetadata)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(description.DefaultValue, modelMetadata.ModelType);
|
||||
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
|
||||
}
|
||||
|
||||
parameter.Required |= description.IsRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents configuration options for rate limiting functionality.
|
||||
/// </summary>
|
||||
/// <remarks>This type is used to define the parameters for controlling the rate at which operations are
|
||||
/// allowed. It specifies the maximum number of permits and the time window during which the permits are
|
||||
/// valid.</remarks>
|
||||
public record RateLimitOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the maximum number of permits that can be issued.
|
||||
/// </summary>
|
||||
public int PermitLimit { get; init; } = 100;
|
||||
/// <summary>
|
||||
/// Gets the time interval that defines the window for rate-limiting operations (in seconds).
|
||||
/// </summary>
|
||||
public int Window { get; init; } = 60;
|
||||
}
|
||||
}
|
||||
42
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml
Normal file
42
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml
Normal file
@@ -0,0 +1,42 @@
|
||||
@page
|
||||
@model Sufi.Demo.PeopleDirectory.UI.Server.Pages.ErrorModel
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Error</title>
|
||||
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="~/css/app.css" rel="stylesheet" asp-append-version="true" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="content px-4">
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
29
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml.cs
Normal file
29
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using System.Diagnostics;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Pages
|
||||
{
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
ui/Sufi.Demo.PeopleDirectory.UI/Server/Program.cs
Normal file
22
ui/Sufi.Demo.PeopleDirectory.UI/Server/Program.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Serilog;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Server;
|
||||
|
||||
try
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.ConfigureServices()
|
||||
.ConfigurePipeline();
|
||||
|
||||
// Ensure all pending migrations are applied.
|
||||
await app.EnsureDatabaseMigrationAsync();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:50212",
|
||||
"sslPort": 44358
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Sufi.Demo.PeopleDirectory.UI.Server": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7024;http://localhost:5243",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
ui/Sufi.Demo.PeopleDirectory.UI/Server/StartupExtensions.cs
Normal file
129
ui/Sufi.Demo.PeopleDirectory.UI/Server/StartupExtensions.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Extensions;
|
||||
using Sufi.Demo.PeopleDirectory.Infrastructure;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Contexts;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Extensions;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Server.Extensions;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Server.Options;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server
|
||||
{
|
||||
public static class StartupExtensions
|
||||
{
|
||||
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
var rateLimitOptions = configuration.GetSection("RateLimit").Get<RateLimitOptions>()!;
|
||||
var services = builder.Services;
|
||||
var licenseKey = configuration.GetValue<string>("LuckyPennyLicenseKey") ?? string.Empty;
|
||||
|
||||
// Add services to the container.
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddApplicationLayer(licenseKey)
|
||||
.AddInfrastructureServices()
|
||||
.AddPersistenceServices(configuration);
|
||||
|
||||
services.AddControllersWithViews();
|
||||
services.AddRazorPages();
|
||||
services.AddHealthChecks();
|
||||
|
||||
services.AddRateLimiter(options =>
|
||||
{
|
||||
// Apply for all requests.
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
||||
RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
factory: partition => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
AutoReplenishment = true,
|
||||
PermitLimit = rateLimitOptions.PermitLimit,
|
||||
Window = TimeSpan.FromSeconds(rateLimitOptions.Window)
|
||||
}));
|
||||
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
});
|
||||
|
||||
// Register Swagger services.
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.RegisterSwagger();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
public static WebApplication ConfigurePipeline(this WebApplication app)
|
||||
{
|
||||
app.UseSerilogRequestLogging(options =>
|
||||
{
|
||||
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
|
||||
{
|
||||
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
|
||||
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent);
|
||||
diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||
};
|
||||
});
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
app.ConfigureSwagger();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.MapHealthChecks("/health");
|
||||
app.MapRazorPages();
|
||||
app.MapControllers();
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public static void ConfigureSwagger(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.RoutePrefix = "swagger";
|
||||
options.DisplayRequestDuration();
|
||||
|
||||
foreach (var desc in ((IEndpointRouteBuilder)app).DescribeApiVersions())
|
||||
{
|
||||
options.SwaggerEndpoint($"{desc.GroupName}/swagger.json",
|
||||
desc.GroupName.ToUpperInvariant());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<IApplicationBuilder> EnsureDatabaseMigrationAsync(this IApplicationBuilder app)
|
||||
{
|
||||
using (var scope = app.ApplicationServices.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
await dbContext.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>19cb1e83-775a-41dd-a289-4d267a75a265</UserSecretsId>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.21" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.21">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Persistence\Sufi.Demo.PeopleDirectory.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Infrastructure\Sufi.Demo.PeopleDirectory.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Persistence\Sufi.Demo.PeopleDirectory.Persistence.csproj" />
|
||||
<ProjectReference Include="..\Client\Sufi.Demo.PeopleDirectory.UI.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnectionString": "Server=localhost:5432;Database=demo-contact;User Id=postgres;Password=Abcd@1234;"
|
||||
},
|
||||
"Serilog": {
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <{Application}>{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <{Application}>{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Seq",
|
||||
"Args": {
|
||||
"serverUrl": "",
|
||||
"apiKey": "",
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <{Application}>{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
37
ui/Sufi.Demo.PeopleDirectory.UI/Server/appsettings.json
Normal file
37
ui/Sufi.Demo.PeopleDirectory.UI/Server/appsettings.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnectionString": ""
|
||||
},
|
||||
"Serilog": {
|
||||
"Using": [
|
||||
"Serilog.Sinks.Console",
|
||||
"Serilog.Sinks.Seq"
|
||||
],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Enrich": [
|
||||
"FromLogContext",
|
||||
"WithProperty:Application"
|
||||
],
|
||||
"Properties": {
|
||||
"Application": "Sufi.Demo.PeopleDirectory"
|
||||
}
|
||||
},
|
||||
"RateLimit": {
|
||||
"PermitLimit": 100,
|
||||
"Window": 60
|
||||
},
|
||||
"LuckyPennyLicenseKey": ""
|
||||
}
|
||||
Reference in New Issue
Block a user