Refactoring
This commit is contained in:
148
.editorconfig
Normal file
148
.editorconfig
Normal file
@@ -0,0 +1,148 @@
|
||||
# Documentation:
|
||||
# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
|
||||
|
||||
# Top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
end_of_line = crlf
|
||||
indent_style = tab
|
||||
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
|
||||
# Suggest explicit accessibility modifiers
|
||||
dotnet_style_require_accessibility_modifiers = always:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_explicit_tuple_names = true:warning
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:none
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:none
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
|
||||
# Definitions
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate, type_parameter
|
||||
dotnet_naming_symbols.methods_properties.applicable_kinds = method, local_function, property
|
||||
dotnet_naming_symbols.public_symbols.applicable_kinds = property, method, field, event
|
||||
dotnet_naming_symbols.public_symbols.applicable_accessibilities = public
|
||||
dotnet_naming_symbols.constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.constant_fields.required_modifiers = const
|
||||
dotnet_naming_symbols.private_protected_internal_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_protected_internal_fields.applicable_accessibilities = private, protected, internal
|
||||
dotnet_naming_symbols.parameters_locals.applicable_kinds = parameter, local
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
dotnet_naming_style.camel_case_style.capitalization = camel_case
|
||||
|
||||
# Name all types using PascalCase
|
||||
dotnet_naming_rule.types_must_be_capitalized.symbols = types
|
||||
dotnet_naming_rule.types_must_be_capitalized.style = pascal_case_style
|
||||
dotnet_naming_rule.types_must_be_capitalized.severity = warning
|
||||
|
||||
# Name all methods and properties using PascalCase
|
||||
dotnet_naming_rule.methods_properties_must_be_capitalized.symbols = methods_properties
|
||||
dotnet_naming_rule.methods_properties_must_be_capitalized.style = pascal_case_style
|
||||
dotnet_naming_rule.methods_properties_must_be_capitalized.severity = warning
|
||||
|
||||
# Name all public members using PascalCase
|
||||
dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols
|
||||
dotnet_naming_rule.public_members_must_be_capitalized.style = pascal_case_style
|
||||
dotnet_naming_rule.public_members_must_be_capitalized.severity = warning
|
||||
|
||||
# Name all constant fields using PascalCase
|
||||
dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields
|
||||
dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case_style
|
||||
dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = suggestion
|
||||
|
||||
# Name all private and internal fields using camelCase
|
||||
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.symbols = private_protected_internal_fields
|
||||
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.style = camel_case_style
|
||||
dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.severity = warning
|
||||
|
||||
# Name all parameters and locals using camelCase
|
||||
dotnet_naming_rule.parameters_locals_must_be_camel_case.symbols = parameters_locals
|
||||
dotnet_naming_rule.parameters_locals_must_be_camel_case.style = camel_case_style
|
||||
dotnet_naming_rule.parameters_locals_must_be_camel_case.severity = warning
|
||||
|
||||
[*.cs]
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
|
||||
|
||||
# Only use "var" when it's obvious what the variable type is
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = false:none
|
||||
|
||||
# Prefer method-like constructs to have a block body
|
||||
csharp_style_expression_bodied_methods = false:none
|
||||
csharp_style_expression_bodied_constructors = false:none
|
||||
csharp_style_expression_bodied_operators = false:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = one_less_than_current
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = do_not_ignore
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
[*.{xml,csproj,targets,props,json}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
564
.gitignore
vendored
Normal file
564
.gitignore
vendored
Normal file
@@ -0,0 +1,564 @@
|
||||
# Created by https://www.gitignore.io/api/linux,macos,windows,aspnetcore,visualstudio,visualstudiocode,vim
|
||||
# Edit at https://www.gitignore.io/?templates=linux,macos,windows,aspnetcore,visualstudio,visualstudiocode,vim
|
||||
|
||||
### ASPNETCore ###
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
nuget.config
|
||||
build
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# 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
|
||||
# TODO: 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
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# 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
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# 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
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/
|
||||
|
||||
### Linux ###
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
### VisualStudio ###
|
||||
## 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/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
|
||||
# Build results
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
# 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
|
||||
|
||||
# NUNIT
|
||||
|
||||
# Build Results of an ATL Project
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_h.h
|
||||
*.iobj
|
||||
*.ipdb
|
||||
*_wpftmp.csproj
|
||||
|
||||
# Chutzpah Test files
|
||||
|
||||
# Visual C++ cache files
|
||||
|
||||
# Visual Studio profiler
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
|
||||
# TeamCity is a build add-in
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
|
||||
# NCrunch
|
||||
|
||||
# MightyMoose
|
||||
|
||||
# Web workbench (sass)
|
||||
|
||||
# Installshield output folder
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
|
||||
# Click-Once directory
|
||||
|
||||
# Publish Web Output
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
|
||||
# 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
|
||||
|
||||
# NuGet Packages
|
||||
# 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
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
|
||||
# Windows Store app package directories and files
|
||||
*.appx
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
# but keep track of directories ending in .cache
|
||||
|
||||
# Others
|
||||
|
||||
# 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
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rptproj.rsuser
|
||||
|
||||
# Microsoft Fakes
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
|
||||
# Visual Studio 6 build log
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
|
||||
# Paket dependency manager
|
||||
|
||||
# FAKE - F# Make
|
||||
|
||||
# JetBrains Rider
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
|
||||
# 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/
|
||||
|
||||
# End of https://www.gitignore.io/api/linux,macos,windows,aspnetcore,visualstudio,visualstudiocode,vim
|
||||
12
.gitlab-ci.yml
Normal file
12
.gitlab-ci.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
image: mcr.microsoft.com/dotnet/sdk
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
build:
|
||||
stage: build
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- bash build.sh
|
||||
- dotnet nuget push -k $APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate build/*.nupkg
|
||||
56
AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj
Normal file
56
AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj
Normal file
@@ -0,0 +1,56 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp3.1;net5.0</TargetFrameworks>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
|
||||
<AssemblyName>AMWD.Common.AspNetCore</AssemblyName>
|
||||
<RootNamespace>AMWD.Common.AspNetCore</RootNamespace>
|
||||
<NrtRevisionFormat>{semvertag:master:+chash}{!:-dirty}</NrtRevisionFormat>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<BuildInParallel>false</BuildInParallel>
|
||||
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<PackageId>AMWD.Common.AspNetCore</PackageId>
|
||||
|
||||
<Product>AM.WD Common Library for ASP.NET Core</Product>
|
||||
<Description>Library with classes and extensions used frequently on AM.WD projects.</Description>
|
||||
<Company>AM.WD</Company>
|
||||
<Authors>Andreas Müller</Authors>
|
||||
<Copyright>© {copyright:2020-} AM.WD</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="2.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="2.0.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.1" />
|
||||
<PackageReference Include="Unclassified.DeepConvert" Version="1.3.0" />
|
||||
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
131
AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs
Normal file
131
AMWD.Common.AspNetCore/Attributes/GoogleReCaptchaAttribute.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom filter attribute to use Google's reCaptcha (v3).
|
||||
/// Usage: [ServiceFilter(typeof(GoogleReCaptchaAttribute))]
|
||||
/// </summary>
|
||||
public class GoogleReCaptchaAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The error key used in <see cref="ActionContext.ModelState"/>.
|
||||
/// </summary>
|
||||
public const string ErrorKey = "GoogleReCaptcha";
|
||||
|
||||
/// <summary>
|
||||
/// The key used in forms submitted to the backend.
|
||||
/// </summary>
|
||||
public const string ResponseTokenKey = "g-recaptcha-response";
|
||||
|
||||
/// <summary>
|
||||
/// The key used in <see cref="Http.HttpContext.Items"/> to transport the score (0 - bot, 1 - human).
|
||||
/// </summary>
|
||||
public const string ScoreKey = "GoogleReCaptchaScore";
|
||||
|
||||
private const string VerificationUrl = "https://www.google.com/recaptcha/api/siteverify";
|
||||
|
||||
private readonly string privateKey;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GoogleReCaptchaAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// appsettings.json:
|
||||
/// <br/>
|
||||
/// <code>
|
||||
/// {<br/>
|
||||
/// [...]<br/>
|
||||
/// "Google": {<br/>
|
||||
/// "ReCaptcha": {<br/>
|
||||
/// "PrivateKey": "__private reCaptcha key__",<br/>
|
||||
/// "PublicKey": "__public reCaptcha key__"<br/>
|
||||
/// }<br/>
|
||||
/// }<br/>
|
||||
/// }
|
||||
/// </code>
|
||||
/// <br/>
|
||||
/// The score from google can be found on HttpContext.Items[GoogleReCaptchaAttribute.ScoreKey].
|
||||
/// </remarks>
|
||||
/// <param name="configuration">The application configuration.</param>
|
||||
public GoogleReCaptchaAttribute(IConfiguration configuration)
|
||||
{
|
||||
privateKey = configuration.GetValue<string>("Google:ReCaptcha:PrivateKey");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the validattion in background.
|
||||
/// </summary>
|
||||
/// <param name="context">The action context.</param>
|
||||
/// <param name="next">The following action delegate.</param>
|
||||
/// <returns>An awaitable task.</returns>
|
||||
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
await DoValidation(context);
|
||||
await base.OnActionExecutionAsync(context, next);
|
||||
}
|
||||
|
||||
private async Task DoValidation(ActionExecutingContext context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(privateKey))
|
||||
return;
|
||||
|
||||
if (!context.HttpContext.Request.HasFormContentType)
|
||||
return;
|
||||
|
||||
var token = context.HttpContext.Request.Form[ResponseTokenKey];
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
context.ModelState.TryAddModelError(ErrorKey, "No token to validate Google reCaptcha");
|
||||
return;
|
||||
}
|
||||
|
||||
await Validate(context, token);
|
||||
}
|
||||
|
||||
private async Task Validate(ActionExecutingContext context, string token)
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
var param = new Dictionary<string, string>
|
||||
{
|
||||
{ "secret", privateKey },
|
||||
{ "response", token }
|
||||
};
|
||||
var response = await httpClient.PostAsync(VerificationUrl, new FormUrlEncodedContent(param));
|
||||
string json = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var result = JsonConvert.DeserializeObject<Response>(json);
|
||||
if (result?.Success != true)
|
||||
{
|
||||
context.ModelState.TryAddModelError(ErrorKey, "Google reCaptcha verification failed");
|
||||
context.HttpContext.Items[ScoreKey] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.HttpContext.Items[ScoreKey] = result.Score;
|
||||
}
|
||||
}
|
||||
|
||||
private class Response
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
public decimal Score { get; set; }
|
||||
|
||||
public string Action { get; set; }
|
||||
|
||||
[JsonProperty("challenge_ts")]
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
public string Hostname { get; set; }
|
||||
|
||||
[JsonProperty("error-codes")]
|
||||
public List<string> ErrorCodes { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="IApplicationBuilder"/>.
|
||||
/// </summary>
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds settings to run behind a reverse proxy (e.g. NginX).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A base path (e.g. running in a sub-directory /app) for the application is defined via<br/>
|
||||
/// - <c>ASPNETCORE_APPL_PATH</c> environment variable (preferred)<br/>
|
||||
/// - <c>AspNetCore_Appl_Path</c> in the settings file<br/>
|
||||
/// <br/>
|
||||
/// Additionally you can specify the proxy server by using <paramref name="address"/> or a <paramref name="network"/> when there are multiple proxy servers.
|
||||
/// <br/>
|
||||
/// When no <paramref name="address"/> oder <paramref name="network"/> is set, the default IPv4 private subnets are configured:<br/>
|
||||
/// - <c>10.0.0.0/8</c><br/>
|
||||
/// - <c>172.16.0.0/12</c><br/>
|
||||
/// - <c>192.168.0.0/16</c>
|
||||
/// </remarks>
|
||||
/// <param name="app">The application builder.</param>
|
||||
/// <param name="network">The <see cref="IPNetwork"/> where proxy requests are received from (optional).</param>
|
||||
/// <param name="address">The <see cref="IPAddress"/> where proxy requests are received from (optional).</param>
|
||||
public static void UseProxyHosting(this IApplicationBuilder app, IPNetwork network = null, IPAddress address = null)
|
||||
{
|
||||
string path = Environment.GetEnvironmentVariable("ASPNETCORE_APPL_PATH");
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
app.UsePathBase(new PathString(path));
|
||||
|
||||
var options = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
||||
options.KnownProxies.Clear();
|
||||
options.KnownNetworks.Clear();
|
||||
|
||||
if (network == null && address == null)
|
||||
{
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
|
||||
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16));
|
||||
}
|
||||
|
||||
if (network != null)
|
||||
options.KnownNetworks.Add(network);
|
||||
|
||||
if (address != null)
|
||||
options.KnownProxies.Add(address);
|
||||
|
||||
app.UseForwardedHeaders(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs
Normal file
80
AMWD.Common.AspNetCore/Extensions/HtmlExtensions.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the HTML (e.g. <see cref="IHtmlHelper"/>).
|
||||
/// </summary>
|
||||
public static class HtmlExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// The prefix used to identify JavaScript parts.
|
||||
/// </summary>
|
||||
public static string JSPrefix { get; set; } = "_JS_";
|
||||
|
||||
/// <summary>
|
||||
/// The prefix used to identify CascadingStyleSheet parts.
|
||||
/// </summary>
|
||||
public static string CSSPrefix { get; set; } = "_CSS_";
|
||||
|
||||
/// <summary>
|
||||
/// Add a js snippet.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <param name="template">The template to use to add the snippet.</param>
|
||||
/// <returns></returns>
|
||||
public static T AddJS<T>(this IHtmlHelper<T> htmlHelper, Func<object, HelperResult> template)
|
||||
{
|
||||
htmlHelper.ViewContext.HttpContext.Items[$"{JSPrefix}{Guid.NewGuid()}"] = template;
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the js snippets into the view.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <returns></returns>
|
||||
public static T RenderJS<T>(this IHtmlHelper<T> htmlHelper)
|
||||
{
|
||||
foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
|
||||
{
|
||||
if (key.ToString().StartsWith(JSPrefix) && htmlHelper.ViewContext.HttpContext.Items[key] is Func<object, HelperResult> template)
|
||||
htmlHelper.ViewContext.Writer.WriteLine(template(null));
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a css snippet.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <param name="template">The template to use to add the snippet.</param>
|
||||
/// <returns></returns>
|
||||
public static T AddCSS<T>(this IHtmlHelper<T> htmlHelper, Func<object, HelperResult> template)
|
||||
{
|
||||
htmlHelper.ViewContext.HttpContext.Items[$"{CSSPrefix}{Guid.NewGuid()}"] = template;
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the css snippets into the view.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dynamic type of the <see cref="IHtmlHelper"/>.</typeparam>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/> instance.</param>
|
||||
/// <returns></returns>
|
||||
public static T RenderCSS<T>(this IHtmlHelper<T> htmlHelper)
|
||||
{
|
||||
foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
|
||||
{
|
||||
if (key.ToString().StartsWith(CSSPrefix) && htmlHelper.ViewContext.HttpContext.Items[key] is Func<object, HelperResult> template)
|
||||
htmlHelper.ViewContext.Writer.WriteLine(template(null));
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs
Normal file
63
AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Antiforgery;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="HttpContext"/>.
|
||||
/// </summary>
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the antiforgery token.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The web context.</param>
|
||||
/// <returns>Name and value of the token.</returns>
|
||||
public static (string Name, string Value) GetAntiforgeryToken(this HttpContext httpContext)
|
||||
{
|
||||
var af = httpContext.RequestServices.GetService<IAntiforgery>();
|
||||
var set = af?.GetAndStoreTokens(httpContext);
|
||||
|
||||
return (Name: set?.FormFieldName, Value: set?.RequestToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the remote ip address.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The web context.</param>
|
||||
/// <param name="headerName">The name of the header to resolve the <see cref="IPAddress"/> when behind a proxy (Default: X-Forwarded-For).</param>
|
||||
/// <returns>The ip address of the client.</returns>
|
||||
public static IPAddress GetRemoteIpAddress(this HttpContext httpContext, string headerName = "X-Forwarded-For")
|
||||
{
|
||||
var remote = httpContext.Connection.RemoteIpAddress;
|
||||
|
||||
string forwardedHeader = httpContext.Request.Headers[headerName].ToString();
|
||||
if (!string.IsNullOrWhiteSpace(forwardedHeader) && IPAddress.TryParse(forwardedHeader, out var forwarded))
|
||||
return forwarded;
|
||||
|
||||
return remote;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the return url.
|
||||
/// </summary>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetReturnUrl(this HttpContext httpContext)
|
||||
{
|
||||
string url = httpContext.Items["OriginalRequest"]?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
url = httpContext.Request.Query["ReturnUrl"].ToString();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a session when available.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
|
||||
public static void ClearSession(this HttpContext httpContext)
|
||||
=> httpContext?.Session?.Clear();
|
||||
}
|
||||
}
|
||||
98
AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs
Normal file
98
AMWD.Common.AspNetCore/Extensions/LoggerExtensions.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
|
||||
namespace Microsoft.Extensions.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="ILogger"/>.
|
||||
/// </summary>
|
||||
internal static class LoggerExtensions
|
||||
{
|
||||
// Found here:
|
||||
// https://github.com/dotnet/aspnetcore/blob/a4c45262fb8549bdb4f5e4f76b16f98a795211ae/src/Mvc/Mvc.Core/src/MvcCoreLoggerExtensions.cs
|
||||
|
||||
public static void AttemptingToBindModel(this ILogger logger, ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!logger.IsEnabled(LogLevel.Debug))
|
||||
return;
|
||||
|
||||
var modelMetadata = bindingContext.ModelMetadata;
|
||||
switch (modelMetadata.MetadataKind)
|
||||
{
|
||||
case ModelMetadataKind.Parameter:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(44, "AttemptingToBindParameterModel"),
|
||||
$"Attempting to bind parameter '{modelMetadata.ParameterName}' of type '{modelMetadata.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Property:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(13, "AttemptingToBindPropertyModel"),
|
||||
$"Attempting to bind property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{modelMetadata.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Type:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(24, "AttemptingToBindModel"),
|
||||
$"Attempting to bind model of type '{bindingContext.ModelType}' using the name '{bindingContext.ModelName}' in request data ...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void FoundNoValueInRequest(this ILogger logger, ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!logger.IsEnabled(LogLevel.Debug))
|
||||
return;
|
||||
|
||||
var modelMetadata = bindingContext.ModelMetadata;
|
||||
switch (modelMetadata.MetadataKind)
|
||||
{
|
||||
case ModelMetadataKind.Parameter:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(16, "FoundNoValueForParameterInRequest"),
|
||||
$"Could not find a value in the request with name '{bindingContext.ModelName}' for binding parameter '{modelMetadata.ParameterName}' of typ//(('{bindingContext.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Property:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(15, "FoundNoValueForPropertyInRequest"),
|
||||
$"Could not find a value in the request with name '{bindingContext.ModelName}' for binding property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{bindingContext.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Type:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(46, "FoundNoValueInRequest"),
|
||||
$"Could not find a value in the request with name '{bindingContext.ModelName}' of type '{bindingContext.ModelType}'.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DoneAttemptingToBindModel(this ILogger logger, ModelBindingContext bindingContext)
|
||||
{
|
||||
if (!logger.IsEnabled(LogLevel.Debug))
|
||||
return;
|
||||
|
||||
var modelMetadata = bindingContext.ModelMetadata;
|
||||
switch (modelMetadata.MetadataKind)
|
||||
{
|
||||
case ModelMetadataKind.Parameter:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(45, "DoneAttemptingToBindParameterModel"),
|
||||
$"Done attempting to bind parameter '{modelMetadata.ParameterName}' of type '{modelMetadata.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Property:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(14, "DoneAttemptingToBindPropertyModel"),
|
||||
$"Done attempting to bind property '{modelMetadata.ContainerType}.{modelMetadata.PropertyName}' of type '{modelMetadata.ModelType}'.");
|
||||
break;
|
||||
|
||||
case ModelMetadataKind.Type:
|
||||
logger.Log(LogLevel.Debug,
|
||||
new EventId(25, "DoneAttemptingToBindModel"),
|
||||
$"Done attempting to bind model of type '{bindingContext.ModelType}' using the name '{bindingContext.ModelName}'.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for the ASP.NET Core application.
|
||||
/// </summary>
|
||||
public static class ModelStateDictionaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the specified <paramref name="errorMessage"/> to the <see cref="ModelStateEntry.Errors"/>
|
||||
/// instance that is associated with the key specified as a <see cref="MemberExpression"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The type of the model.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property.</typeparam>
|
||||
/// <param name="modelState">The <see cref="ModelStateDictionary"/> instance.</param>
|
||||
/// <param name="model">The model. Only used to infer the model type.</param>
|
||||
/// <param name="keyExpression">The <see cref="MemberExpression"/> that specifies the property.</param>
|
||||
/// <param name="errorMessage">The error message to add.</param>
|
||||
/// <exception cref="InvalidOperationException">No member expression provided.</exception>
|
||||
#pragma warning disable IDE0060 // remove unused parameters
|
||||
public static void AddModelError<TModel, TProperty>(this ModelStateDictionary modelState, TModel model, Expression<Func<TModel, TProperty>> keyExpression, string errorMessage)
|
||||
#pragma warning restore IDE0060 // remove unused parameters
|
||||
{
|
||||
if (modelState is null)
|
||||
throw new ArgumentNullException(nameof(modelState));
|
||||
|
||||
string key = "";
|
||||
var expr = keyExpression.Body as MemberExpression;
|
||||
while (expr != null)
|
||||
{
|
||||
key = expr.Member.Name + (key != "" ? "." + key : "");
|
||||
expr = expr.Expression as MemberExpression;
|
||||
}
|
||||
if (key == "")
|
||||
throw new InvalidOperationException("No member expression provided.");
|
||||
|
||||
modelState.AddModelError(key, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a hosted service that is instanciated only once.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The type of the service to add.</typeparam>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static IServiceCollection AddSingletonHostedService<TService>(this IServiceCollection services)
|
||||
where TService : class, IHostedService
|
||||
{
|
||||
services.AddSingleton<TService>();
|
||||
services.AddSingleton<IHostedService, BackgroundServiceStarter<TService>>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
AMWD.Common.AspNetCore/Extensions/SessionExtensions.cs
Normal file
56
AMWD.Common.AspNetCore/Extensions/SessionExtensions.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="ISession"/> object.
|
||||
/// </summary>
|
||||
public static class SessionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a strong typed value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
public static void SetValue<T>(this ISession session, string key, T value)
|
||||
=> session.SetString(key, JsonConvert.SerializeObject(value));
|
||||
|
||||
/// <summary>
|
||||
/// Gets a strong typed value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The value.</returns>
|
||||
public static T GetValue<T>(this ISession session, string key)
|
||||
=> session.HasKey(key) ? JsonConvert.DeserializeObject<T>(session.GetString(key)) : default;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a strong typed value or the fallback value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="fallback">A fallback value when the key is not present.</param>
|
||||
/// <returns>The value.</returns>
|
||||
public static T GetValue<T>(this ISession session, string key, T fallback)
|
||||
{
|
||||
if (session.HasKey(key))
|
||||
return session.GetValue<T>(key);
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the session has the key available.
|
||||
/// </summary>
|
||||
/// <param name="session">The current session.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns><c>true</c> when the key was found, otherwise <c>false</c>.</returns>
|
||||
public static bool HasKey(this ISession session, string key)
|
||||
=> session.Keys.Contains(key);
|
||||
}
|
||||
}
|
||||
68
AMWD.Common.AspNetCore/Middlewares/BasicAuthMiddleware.cs
Normal file
68
AMWD.Common.AspNetCore/Middlewares/BasicAuthMiddleware.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a basic authentication.
|
||||
/// </summary>
|
||||
public class BasicAuthMiddleware
|
||||
{
|
||||
private readonly RequestDelegate next;
|
||||
private readonly string realm;
|
||||
private readonly Func<string, string, bool> userPasswordAuth;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BasicAuthMiddleware"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The following delegate in the process chain.</param>
|
||||
/// <param name="realm">The realm to display when requesting for credentials.</param>
|
||||
/// <param name="userPasswordAuth">The function <c>(user, passwd) => result</c> to validate username and password.</param>
|
||||
public BasicAuthMiddleware(RequestDelegate next, string realm, Func<string, string, bool> userPasswordAuth)
|
||||
{
|
||||
this.next = next;
|
||||
this.realm = realm;
|
||||
this.userPasswordAuth = userPasswordAuth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The delegate invokation.
|
||||
/// Performs the authentication check.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The corresponding HTTP context.</param>
|
||||
/// <returns>An awaitable task.</returns>
|
||||
public async Task InvokeAsync(HttpContext httpContext)
|
||||
{
|
||||
if (httpContext.Request.Headers.TryGetValue("Authorization", out var authHeader)
|
||||
&& ((string)authHeader).StartsWith("Basic "))
|
||||
{
|
||||
string encoded = ((string)authHeader).Split(' ', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? "";
|
||||
|
||||
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
|
||||
string[] parts = decoded.Split(':');
|
||||
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
string username = parts[0].Trim().ToLower();
|
||||
string password = parts[1].Trim();
|
||||
|
||||
if (userPasswordAuth(username, password))
|
||||
{
|
||||
await next.Invoke(httpContext);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
httpContext.Response.Headers["WWW-Authenticate"] = "Basic";
|
||||
if (!string.IsNullOrWhiteSpace(realm))
|
||||
{
|
||||
httpContext.Response.Headers["WWW-Authenticate"] += $" realm=\"{realm}\"";
|
||||
}
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their <see href="https://github.com/dotnet/aspnetcore/issues/6566">issue</see> with other cultures than en-US.
|
||||
/// </summary>
|
||||
public class CustomFloatingPointModelBinder : IModelBinder
|
||||
{
|
||||
private readonly NumberStyles supportedNumberStyles;
|
||||
private readonly ILogger logger;
|
||||
private readonly CultureInfo cultureInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="CustomFloatingPointModelBinder"/>.
|
||||
/// </summary>
|
||||
/// <param name="supportedStyles">The <see cref="NumberStyles"/>.</param>
|
||||
/// <param name="cultureInfo">The <see cref="CultureInfo"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public CustomFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory)
|
||||
{
|
||||
this.cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo));
|
||||
|
||||
supportedNumberStyles = supportedStyles;
|
||||
logger = loggerFactory?.CreateLogger<CustomFloatingPointModelBinder>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext == null)
|
||||
throw new ArgumentNullException(nameof(bindingContext));
|
||||
|
||||
logger?.AttemptingToBindModel(bindingContext);
|
||||
string modelName = bindingContext.ModelName;
|
||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
|
||||
if (valueProviderResult == ValueProviderResult.None)
|
||||
{
|
||||
logger?.FoundNoValueInRequest(bindingContext);
|
||||
|
||||
// no entry
|
||||
logger?.DoneAttemptingToBindModel(bindingContext);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var modelState = bindingContext.ModelState;
|
||||
modelState.SetModelValue(modelName, valueProviderResult);
|
||||
|
||||
var metadata = bindingContext.ModelMetadata;
|
||||
var type = metadata.UnderlyingOrModelType;
|
||||
try
|
||||
{
|
||||
string value = valueProviderResult.FirstValue;
|
||||
var culture = cultureInfo ?? valueProviderResult.Culture;
|
||||
|
||||
object model;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
|
||||
model = null;
|
||||
}
|
||||
else if (type == typeof(float))
|
||||
{
|
||||
model = float.Parse(value, supportedNumberStyles, culture);
|
||||
}
|
||||
else if (type == typeof(double))
|
||||
{
|
||||
model = double.Parse(value, supportedNumberStyles, culture);
|
||||
}
|
||||
else if (type == typeof(decimal))
|
||||
{
|
||||
model = decimal.Parse(value, supportedNumberStyles, culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
// unreachable
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// When converting value, a null model may indicate a failed conversion for an otherwise required
|
||||
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
|
||||
// current bindingContext. If not, an error is logged.
|
||||
if (model == null && !metadata.IsReferenceOrNullableType)
|
||||
{
|
||||
modelState.TryAddModelError(
|
||||
modelName,
|
||||
metadata
|
||||
.ModelBindingMessageProvider
|
||||
.ValueMustNotBeNullAccessor(valueProviderResult.ToString())
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
bool isFormatException = exception is FormatException;
|
||||
if (!isFormatException && exception.InnerException != null)
|
||||
{
|
||||
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
|
||||
// this code in case a cursory review of the CoreFx code missed something.
|
||||
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
|
||||
}
|
||||
|
||||
modelState.TryAddModelError(modelName, exception, metadata);
|
||||
// Conversion failed.
|
||||
}
|
||||
|
||||
logger?.DoneAttemptingToBindModel(bindingContext);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IModelBinderProvider"/> for binding <see cref="decimal"/>, <see cref="double"/>,
|
||||
/// <see cref="float"/>, and their <see cref="Nullable{T}"/> wrappers.
|
||||
/// Modified to set <see cref="NumberStyles"/> and <see cref="System.Globalization.CultureInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To use this provider, insert it at the beginning of the providers list:<br/>
|
||||
/// <code>
|
||||
/// services.AddControllersWithViews(options =><br/>
|
||||
/// {<br/>
|
||||
/// options.ModelBinderProviders.Insert(0, new CustomFloatingPointModelBinderProvider());<br/>
|
||||
/// });</code>
|
||||
/// </remarks>
|
||||
public class CustomFloatingPointModelBinderProvider : IModelBinderProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the supported <see cref="NumberStyles"/> globally.
|
||||
/// Default: <see cref="NumberStyles.Float"/> and <see cref="NumberStyles.AllowThousands"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="SimpleTypeModelBinder"/> uses <see cref="DecimalConverter"/> and similar. Those <see cref="TypeConverter"/>s default to <see cref="NumberStyles.Float"/>.
|
||||
/// </remarks>
|
||||
public static NumberStyles SupportedNumberStyles { get; set; } = NumberStyles.Float | NumberStyles.AllowThousands;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="System.Globalization.CultureInfo"/> to use while parsing globally.
|
||||
/// Default: <see cref="CultureInfo.InvariantCulture"/>.
|
||||
/// </summary>
|
||||
public static CultureInfo CultureInfo { get; set; } = CultureInfo.InvariantCulture;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IModelBinder GetBinder(ModelBinderProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
|
||||
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
|
||||
var modelType = context.Metadata.UnderlyingOrModelType;
|
||||
if (modelType == typeof(decimal) ||
|
||||
modelType == typeof(double) ||
|
||||
modelType == typeof(float))
|
||||
{
|
||||
return new CustomFloatingPointModelBinder(SupportedNumberStyles, CultureInfo, loggerFactory);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
61
AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs
Normal file
61
AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelpers
|
||||
{
|
||||
// Source: https://stackoverflow.com/a/42385059
|
||||
|
||||
/// <summary>
|
||||
/// A tag helper that adds a CSS class attribute based on a condition.
|
||||
/// </summary>
|
||||
[HtmlTargetElement(Attributes = ClassPrefix + "*")]
|
||||
public class ConditionClassTagHelper : TagHelper
|
||||
{
|
||||
private const string ClassPrefix = "condition-class-";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unconditional CSS class attribute value of the element.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("class")]
|
||||
public string CssClass { get; set; }
|
||||
|
||||
private IDictionary<string, bool> classValues;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a dictionary containing all conditional class names and a boolean condition
|
||||
/// value indicating whether the class should be added to the element.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("", DictionaryAttributePrefix = ClassPrefix)]
|
||||
public IDictionary<string, bool> ClassValues
|
||||
{
|
||||
get
|
||||
{
|
||||
return classValues ??= new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
set
|
||||
{
|
||||
classValues = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronously executes the <see cref="T:Microsoft.AspNetCore.Razor.TagHelpers.TagHelper"/>
|
||||
/// with the given <paramref name="context"/> and <paramref name="output"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains information associated with the current HTML tag.</param>
|
||||
/// <param name="output">A stateful HTML element used to generate an HTML tag.</param>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
var items = classValues.Where(e => e.Value).Select(e => e.Key).ToList();
|
||||
if (!string.IsNullOrEmpty(CssClass))
|
||||
items.Insert(0, CssClass);
|
||||
|
||||
if (items.Any())
|
||||
{
|
||||
string classes = string.Join(" ", items.ToArray());
|
||||
output.Attributes.Add("class", classes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs
Normal file
38
AMWD.Common.AspNetCore/TagHelpers/EmailTagHelper.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// A tag helper to create a obfuscated email link.
|
||||
/// </summary>
|
||||
[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)]
|
||||
public class EmailTagHelper : TagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The e-mail address.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("asp-address")]
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Processes the element.
|
||||
/// </summary>
|
||||
/// <param name="context">The tag helper context.</param>
|
||||
/// <param name="output">The tag helper output.</param>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"mailto:{Address}"));
|
||||
string reversed = new(Address.Reverse().ToArray());
|
||||
|
||||
output.TagName = "a";
|
||||
output.TagMode = TagMode.StartTagAndEndTag;
|
||||
output.Attributes.SetAttribute("href", new HtmlString($"javascript:window.location.href=atob('{base64}')"));
|
||||
output.Attributes.SetAttribute("style", "unicode-bidi: bidi-override; direction: rtl;");
|
||||
output.Attributes.RemoveAll("asp-address");
|
||||
output.Content.SetContent(reversed);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs
Normal file
186
AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds additional behavior to the modelbinding for numeric properties.
|
||||
/// </summary>
|
||||
[HtmlTargetElement("input", Attributes = "asp-for")]
|
||||
public class NumberInputTagHelper : InputTagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NumberInputTagHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="generator">The HTML generator.</param>
|
||||
public NumberInputTagHelper(IHtmlGenerator generator)
|
||||
: base(generator)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
base.Process(context, output);
|
||||
|
||||
var types = new[] {
|
||||
typeof(byte), typeof(sbyte),
|
||||
typeof(ushort), typeof(short),
|
||||
typeof(uint), typeof(int),
|
||||
typeof(ulong), typeof(long),
|
||||
typeof(float),
|
||||
typeof(double),
|
||||
typeof(decimal)
|
||||
};
|
||||
|
||||
var typeAttributes = output.Attributes
|
||||
.Where(a => a.Name == "type")
|
||||
.ToList();
|
||||
string typeAttributeValue = typeAttributes.First().Value as string;
|
||||
|
||||
Type modelType = For.ModelExplorer.ModelType;
|
||||
Type nullableType = Nullable.GetUnderlyingType(modelType);
|
||||
|
||||
// the type itself or its nullable wrapper matching and
|
||||
// the type attribute is number or there is only one type attribute
|
||||
// IMPORTANT TO KNOW: if the type attribute is set in the view, there are two attributes with same value.
|
||||
if ((types.Contains(modelType) || types.Contains(nullableType)) && (typeAttributeValue == "number" || typeAttributes.Count == 1))
|
||||
{
|
||||
var culture = CultureInfo.InvariantCulture;
|
||||
string min = "";
|
||||
string max = "";
|
||||
string step = "";
|
||||
string value = "";
|
||||
|
||||
if (modelType == typeof(byte) || nullableType == typeof(byte))
|
||||
{
|
||||
min = byte.MinValue.ToString(culture);
|
||||
max = byte.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
byte val = (byte)For.Model;
|
||||
value = value.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(sbyte) || nullableType == typeof(sbyte))
|
||||
{
|
||||
min = sbyte.MinValue.ToString(culture);
|
||||
max = sbyte.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
sbyte val = (sbyte)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(ushort) || nullableType == typeof(ushort))
|
||||
{
|
||||
min = ushort.MinValue.ToString(culture);
|
||||
max = ushort.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
ushort val = (ushort)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(short) || nullableType == typeof(short))
|
||||
{
|
||||
min = short.MinValue.ToString(culture);
|
||||
max = short.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
short val = (short)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(uint) || nullableType == typeof(uint))
|
||||
{
|
||||
min = uint.MinValue.ToString(culture);
|
||||
max = uint.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
uint val = (uint)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(int) || nullableType == typeof(int))
|
||||
{
|
||||
min = int.MinValue.ToString(culture);
|
||||
max = int.MaxValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
int val = (int)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(ulong) || nullableType == typeof(ulong))
|
||||
{
|
||||
min = ulong.MinValue.ToString(culture);
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
ulong val = (ulong)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(long) || nullableType == typeof(long))
|
||||
{
|
||||
if (For.Model != null)
|
||||
{
|
||||
long val = (long)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(float) || nullableType == typeof(float))
|
||||
{
|
||||
step = "any";
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
float val = (float)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(double) || nullableType == typeof(double))
|
||||
{
|
||||
step = "any";
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
double val = (double)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
else if (modelType == typeof(decimal) || nullableType == typeof(decimal))
|
||||
{
|
||||
step = "any";
|
||||
|
||||
if (For.Model != null)
|
||||
{
|
||||
decimal val = (decimal)For.Model;
|
||||
value = val.ToString(culture);
|
||||
}
|
||||
}
|
||||
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("type", "number"));
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("value", value));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(min) && !output.Attributes.ContainsName("min"))
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("min", min));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(max) && !output.Attributes.ContainsName("max"))
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("max", max));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(step) && !output.Attributes.ContainsName("step"))
|
||||
output.Attributes.SetAttribute(new TagHelperAttribute("step", step));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
AMWD.Common.AspNetCore/Utilities/BackgroundServiceStarter.cs
Normal file
44
AMWD.Common.AspNetCore/Utilities/BackgroundServiceStarter.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper class to start a background service.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">The service type.</typeparam>
|
||||
public class BackgroundServiceStarter<TService> : IHostedService
|
||||
where TService : class, IHostedService
|
||||
{
|
||||
private readonly TService service;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an new instance of the <see cref="BackgroundServiceStarter{TService}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="backgroundService">The service to work in background.</param>
|
||||
public BackgroundServiceStarter(TService backgroundService)
|
||||
{
|
||||
service = backgroundService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return service.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the service.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return service.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
AMWD.Common.AspNetCore/Utilities/PasswordHelper.cs
Normal file
52
AMWD.Common.AspNetCore/Utilities/PasswordHelper.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace Microsoft.AspNetCore.Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides password hashing and verification methods.
|
||||
/// </summary>
|
||||
public static class PasswordHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Hashes a password.
|
||||
/// </summary>
|
||||
/// <param name="plainPassword">The plain password.</param>
|
||||
/// <returns></returns>
|
||||
public static string HashPassword(string plainPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(plainPassword))
|
||||
return plainPassword?.Trim();
|
||||
|
||||
var ph = new PasswordHasher<object>();
|
||||
return ph.HashPassword(null, plainPassword.Trim());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a password with a hashed version.
|
||||
/// </summary>
|
||||
/// <param name="plainPassword">The plain password.</param>
|
||||
/// <param name="hashedPassword">The password hash.</param>
|
||||
/// <param name="rehashNeeded">A value indicating whether the password needs a rehash.</param>
|
||||
/// <returns></returns>
|
||||
public static bool VerifyPassword(string plainPassword, string hashedPassword, out bool rehashNeeded)
|
||||
{
|
||||
rehashNeeded = false;
|
||||
if (string.IsNullOrWhiteSpace(plainPassword) || string.IsNullOrWhiteSpace(hashedPassword))
|
||||
return false;
|
||||
|
||||
var ph = new PasswordHasher<object>();
|
||||
var result = ph.VerifyHashedPassword(null, hashedPassword, plainPassword);
|
||||
switch (result)
|
||||
{
|
||||
case PasswordVerificationResult.Success:
|
||||
return true;
|
||||
|
||||
case PasswordVerificationResult.SuccessRehashNeeded:
|
||||
rehashNeeded = true;
|
||||
return true;
|
||||
|
||||
case PasswordVerificationResult.Failed:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp3.1;net5.0</TargetFrameworks>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
|
||||
<AssemblyName>AMWD.Common.EntityFrameworkCore</AssemblyName>
|
||||
<RootNamespace>AMWD.Common.EntityFrameworkCore</RootNamespace>
|
||||
<NrtRevisionFormat>{semvertag:master:+chash}{!:-dirty}</NrtRevisionFormat>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<BuildInParallel>false</BuildInParallel>
|
||||
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<PackageId>AMWD.Common.EntityFrameworkCore</PackageId>
|
||||
|
||||
<Product>AM.WD Common Library for EntityFramework Core</Product>
|
||||
<Description>Library with classes and extensions used frequently on AM.WD projects.</Description>
|
||||
<Company>AM.WD</Company>
|
||||
<Authors>Andreas Müller</Authors>
|
||||
<Copyright>© {copyright:2020-} AM.WD</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using AMWD.Common.EntityFrameworkCore.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AMWD.Common.EntityFrameworkCore.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Property attribute to create indices and unique constraints in the database.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Requires <see cref="ModelBuilderExtensions.ApplyIndexAttributes(ModelBuilder)"/> to be called within <see cref="DbContext.OnModelCreating(ModelBuilder)"/>.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
|
||||
public class DatabaseIndexAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseIndexAttribute"/> class.
|
||||
/// </summary>
|
||||
public DatabaseIndexAttribute()
|
||||
{
|
||||
Name = null;
|
||||
IsUnique = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a name.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether it is a unique constraint.
|
||||
/// </summary>
|
||||
public bool IsUnique { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// A DatabaseProvider specific exception.
|
||||
/// </summary>
|
||||
public class DatabaseProviderException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseProviderException"/> class.
|
||||
/// </summary>
|
||||
public DatabaseProviderException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseProviderException"/> class
|
||||
/// with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public DatabaseProviderException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseProviderException"/> class
|
||||
/// with a specified error message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public DatabaseProviderException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseProviderException"/> class with serialized data.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
|
||||
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
|
||||
/// <exception cref="ArgumentNullException">The info parameter is null.</exception>
|
||||
/// <exception cref="SerializationException">The class name is null or <see cref="Exception.HResult"/> is zero (0).</exception>
|
||||
protected DatabaseProviderException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.EntityFrameworkCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="DatabaseFacade"/>.
|
||||
/// </summary>
|
||||
public static class DatabaseFacadeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies migration files to the database.
|
||||
/// </summary>
|
||||
/// <param name="database">The database connection.</param>
|
||||
/// <param name="optionsAction">An action to set additional options.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>true on success, otherwise false or an exception is thrown.</returns>
|
||||
public static async Task<bool> ApplyMigrationsAsync(this DatabaseFacade database, Action<DatabaseMigrationOptions> optionsAction, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (database == null)
|
||||
throw new ArgumentNullException(nameof(database));
|
||||
|
||||
var options = new DatabaseMigrationOptions();
|
||||
optionsAction?.Invoke(options);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.MigrationsTableName))
|
||||
throw new ArgumentNullException(nameof(options.MigrationsTableName), $"The property {nameof(options.MigrationsTableName)} of the {nameof(options)} parameter is required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.Path))
|
||||
throw new ArgumentNullException(nameof(options.Path), $"The property {nameof(options.Path)} of the {nameof(options)} parameter is required.");
|
||||
|
||||
var connection = database.GetDbConnection();
|
||||
try
|
||||
{
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
if (!await connection.CreateMigrationsTable(options, cancellationToken))
|
||||
return false;
|
||||
|
||||
return await connection.Migrate(options, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private static DatabaseProvider GetProviderType(this DbConnection connection)
|
||||
{
|
||||
string provider = connection.GetType().FullName;
|
||||
|
||||
if (provider.Contains("mysql", StringComparison.OrdinalIgnoreCase))
|
||||
return DatabaseProvider.MySQL;
|
||||
if (provider.Contains("oracle", StringComparison.OrdinalIgnoreCase))
|
||||
return DatabaseProvider.Oracle;
|
||||
if (provider.Contains("npgsql", StringComparison.OrdinalIgnoreCase))
|
||||
return DatabaseProvider.PostgreSQL;
|
||||
if (provider.Contains("sqlite", StringComparison.OrdinalIgnoreCase))
|
||||
return DatabaseProvider.SQLite;
|
||||
if (provider.Contains("sqlclient", StringComparison.OrdinalIgnoreCase))
|
||||
return DatabaseProvider.SQLServer;
|
||||
|
||||
throw new DatabaseProviderException($"The database provider '{provider}' is unknown");
|
||||
}
|
||||
|
||||
private static async Task<bool> CreateMigrationsTable(this DbConnection connection, DatabaseMigrationOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
|
||||
#pragma warning disable CS8524 // missing default case
|
||||
command.CommandText = connection.GetProviderType() switch
|
||||
#pragma warning restore CS8524 // missing default case
|
||||
{
|
||||
DatabaseProvider.MySQL => $@"CREATE TABLE IF NOT EXISTS `{options.MigrationsTableName}` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`schema_file` VARCHAR(250) NOT NULL,
|
||||
`installed_at` VARCHAR(16) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
);",
|
||||
DatabaseProvider.Oracle => $@"DECLARE ncount NUMBER;
|
||||
BEGIN
|
||||
SELECT count(*) INTO ncount FROM dba_tables WHERE table_name = '{options.MigrationsTableName}';
|
||||
IF (ncount <= 0)
|
||||
THEN
|
||||
EXECUTE IMMEDIATE 'CREATE TABLE ""{options.MigrationsTableName}"" (
|
||||
""id"" NUMBER GENERATED by default on null as IDENTITY,
|
||||
""schema_file"" VARCHAR2(250) NOT NULL,
|
||||
""installed_at"" VARCHAR2(16) NOT NULL,
|
||||
PRIMARY KEY (""id""),
|
||||
CONSTRAINT uq_schema_file UNIQUE (""schema_file"")
|
||||
)';
|
||||
END IF;
|
||||
END;",
|
||||
DatabaseProvider.PostgreSQL => $@"CREATE TABLE IF NOT EXISTS ""{options.MigrationsTableName}"" (
|
||||
""id"" SERIAL4 PRIMARY KEY,
|
||||
""schema_file"" VARCHAR(250) NOT NULL,
|
||||
""installed_at"" VARCHAR(16) NOT NULL,
|
||||
CONSTRAINT ""uq_schema_file"" UNIQUE (""schema_file"")
|
||||
);",
|
||||
DatabaseProvider.SQLite => $@"CREATE TABLE IF NOT EXISTS ""{options.MigrationsTableName}"" (
|
||||
""id"" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
""schema_file"" TEXT(250) NOT NULL,
|
||||
""installed_at"" TEXT(16) NOT NULL,
|
||||
CONSTRAINT ""uq_schema_file"" UNIQUE (""schema_file"")
|
||||
);",
|
||||
DatabaseProvider.SQLServer => $@"IF NOT EXISTS (SELECT * FROM [sysobjects] WHERE [name] = '{options.MigrationsTableName}' AND [xtype] = 'U')
|
||||
BEGIN
|
||||
CREATE TABLE [{options.MigrationsTableName}] (
|
||||
[id] int IDENTITY(1,1) NOT NULL PRIMARY KEY,
|
||||
[schema_file] varchar(250) NOT NULL,
|
||||
[installed_at] varchar(16) NOT NULL,
|
||||
CONSTRAINT uq_schema_file UNIQUE (schema_file)
|
||||
)
|
||||
END;"
|
||||
};
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
options.Logger?.LogCritical(ex, $"Creating migrations table '{options.MigrationsTableName}' failed: {ex.InnerException?.Message ?? ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> Migrate(this DbConnection connection, DatabaseMigrationOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<string> availableMigrationFiles;
|
||||
if (options.SourceAssembly == null)
|
||||
{
|
||||
availableMigrationFiles = Directory.GetFiles(options.Path)
|
||||
.Where(f => f.ToLower().StartsWith(options.Path.ToLower()))
|
||||
.Where(f => f.ToLower().EndsWith(".sql"))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
availableMigrationFiles = options.SourceAssembly
|
||||
.GetManifestResourceNames()
|
||||
.Where(f => f.ToLower().StartsWith(options.Path.ToLower()))
|
||||
.Where(f => f.ToLower().EndsWith(".sql"))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (!availableMigrationFiles.Any())
|
||||
return true;
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
|
||||
var migratedFiles = new List<string>();
|
||||
command.CommandText = connection.GetProviderType() switch
|
||||
{
|
||||
DatabaseProvider.MySQL => $"SELECT `schema_file` FROM `{options.MigrationsTableName}`;",
|
||||
DatabaseProvider.SQLServer => $"SELECT [schema_file] FROM [{options.MigrationsTableName}];",
|
||||
_ => $@"SELECT ""schema_file"" FROM ""{options.MigrationsTableName}"";",
|
||||
};
|
||||
using (var reader = await command.ExecuteReaderAsync(cancellationToken))
|
||||
{
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
migratedFiles.Add(reader.GetString(0));
|
||||
}
|
||||
|
||||
int pathLength = options.Path.Length + 1;
|
||||
foreach (string migrationFile in availableMigrationFiles)
|
||||
{
|
||||
// remove path including the separator
|
||||
string fileName = migrationFile.Replace(options.Path, "")[1..];
|
||||
using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// max length in the database: 250 chars
|
||||
string trimmedFileName = fileName;
|
||||
if (trimmedFileName.Length > 250)
|
||||
fileName = fileName.Substring(0, 250);
|
||||
|
||||
if (migratedFiles.Contains(trimmedFileName))
|
||||
{
|
||||
options.Logger?.LogDebug($" Migrating file '{fileName}' done");
|
||||
continue;
|
||||
}
|
||||
|
||||
string sqlScript = null;
|
||||
if (options.SourceAssembly == null)
|
||||
{
|
||||
sqlScript = await File.ReadAllTextAsync(migrationFile, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var stream = options.SourceAssembly.GetManifestResourceStream(migrationFile);
|
||||
using var sr = new StreamReader(stream);
|
||||
sqlScript = await sr.ReadToEndAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sqlScript))
|
||||
continue;
|
||||
|
||||
options.Logger?.LogDebug($" Migrating file '{fileName}' started");
|
||||
command.Transaction = transaction;
|
||||
|
||||
await command.ExecuteScript(sqlScript, cancellationToken);
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
command.Transaction = null;
|
||||
options.Logger?.LogDebug($" Migrating file '{fileName}' successful");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
options.Logger?.LogError($"Migrating file '{fileName}' failed: {ex.InnerException?.Message ?? ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
options.Logger?.LogCritical(ex, $"Migrating the database failed ({ex.GetType().Name}): {ex.InnerException?.Message ?? ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteScript(this DbCommand command, string text, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.Connection.GetProviderType() == DatabaseProvider.Oracle)
|
||||
{
|
||||
int affectedRows = 0;
|
||||
// Split script by a single slash in a line
|
||||
string[] parts = Regex.Split(text, @"\r?\n[ \t]*/[ \t]*\r?\n");
|
||||
foreach (string part in parts)
|
||||
{
|
||||
// Make writable copy
|
||||
string pt = part;
|
||||
|
||||
// Remove the trailing semicolon from commands where they're not supported
|
||||
// (Oracle doesn't like semicolons. To keep the semicolon, it must be directly
|
||||
// preceeded by "end".)
|
||||
pt = Regex.Replace(pt.TrimEnd(), @"(?<!end);$", "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
// Execute all non-empty parts as individual commands
|
||||
if (!string.IsNullOrWhiteSpace(pt))
|
||||
{
|
||||
command.CommandText = pt;
|
||||
affectedRows += await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
return affectedRows;
|
||||
}
|
||||
else
|
||||
{
|
||||
command.CommandText = text;
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private enum DatabaseProvider
|
||||
{
|
||||
MySQL = 1,
|
||||
Oracle = 2,
|
||||
PostgreSQL = 3,
|
||||
SQLite = 4,
|
||||
SQLServer = 5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.EntityFrameworkCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Extends the <see cref="DbContextOptionsBuilder"/> to use a configurable database provider.
|
||||
/// </summary>
|
||||
public static class DbContextOptionsBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the supported database provider to the context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The configuration provided requires the following entries:
|
||||
/// <list type="bullet">
|
||||
/// <item><strong>Provider</strong>: MySQL | Oracle | PostgreSQL | SQLite | SQLServer</item>
|
||||
/// <item><strong>Host</strong>: hostname or IP address</item>
|
||||
/// <item><strong>Port</strong>: port number</item>
|
||||
/// <item><strong>Name</strong>: database name</item>
|
||||
/// <item><strong>Schema</strong>: schema or search path (e.g. PostgreSQL: public)</item>
|
||||
/// <item><strong>Username</strong>: username credential on the database</item>
|
||||
/// <item><strong>Password</strong>: password credential on the database</item>
|
||||
/// <item><strong>File</strong>: file name / path (for SQLite)</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="optionsBuilder">The options builder.</param>
|
||||
/// <param name="configuration">The application configuration section for the database.</param>
|
||||
/// <param name="optionsAction">An optional action to set additional options.</param>
|
||||
/// <returns>The <see cref="DbContextOptionsBuilder"/> with applied settings.</returns>
|
||||
public static DbContextOptionsBuilder UseDatabaseProvider(this DbContextOptionsBuilder optionsBuilder, IConfiguration configuration, Action<DatabaseProviderOptions> optionsAction = null)
|
||||
{
|
||||
if (optionsBuilder == null)
|
||||
throw new ArgumentNullException(nameof(optionsBuilder));
|
||||
|
||||
if (configuration == null)
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
|
||||
var options = new DatabaseProviderOptions();
|
||||
optionsAction?.Invoke(options);
|
||||
|
||||
string connectionString = GetConnectionString(configuration, options);
|
||||
string provider = configuration.GetValue<string>("provider")?.ToLower();
|
||||
|
||||
var builderType = GetBuilderType(configuration);
|
||||
var extensionType = GetExtensionType(configuration);
|
||||
var actionType = typeof(Action<>).MakeGenericType(builderType);
|
||||
|
||||
object serverVersion = null;
|
||||
MethodInfo methodInfo;
|
||||
switch (provider)
|
||||
{
|
||||
case "mysql":
|
||||
methodInfo = extensionType.GetMethod("UseMySql", new Type[] { typeof(DbContextOptionsBuilder), typeof(string), actionType });
|
||||
if (methodInfo == null)
|
||||
methodInfo = extensionType.GetMethod("UseMySQL", new Type[] { typeof(DbContextOptionsBuilder), typeof(string), actionType });
|
||||
if (methodInfo == null) // Pomelo MySQL v5
|
||||
{
|
||||
var serverVersionType = Type.GetType("Microsoft.EntityFrameworkCore.ServerVersion, Pomelo.EntityFrameworkCore.MySql");
|
||||
var autoDetectMethodInfo = serverVersionType.GetMethod("AutoDetect", new Type[] { typeof(string) });
|
||||
methodInfo = extensionType.GetMethod("UseMySql", new Type[] { typeof(DbContextOptionsBuilder), typeof(string), serverVersionType, actionType });
|
||||
serverVersion = autoDetectMethodInfo.Invoke(null, new object[] { connectionString });
|
||||
}
|
||||
break;
|
||||
case "oracle":
|
||||
methodInfo = extensionType.GetMethod("UseOracle", new Type[] { typeof(DbContextOptionsBuilder), typeof(string), actionType });
|
||||
break;
|
||||
case "postgres":
|
||||
case "postgresql":
|
||||
methodInfo = extensionType.GetMethod("UseNpgsql", new Type[] { typeof(DbContextOptionsBuilder), typeof(string), actionType });
|
||||
break;
|
||||
case "sqlite":
|
||||
methodInfo = extensionType.GetMethod("UseSqlite", new Type[] { typeof(DbContextOptionsBuilder), typeof(string), actionType });
|
||||
break;
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
methodInfo = extensionType.GetMethod("UseSqlServer", new Type[] { typeof(DbContextOptionsBuilder), typeof(string), actionType });
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseProviderException($"Unknown database provider: {provider}");
|
||||
}
|
||||
|
||||
if (serverVersion == null)
|
||||
{
|
||||
methodInfo?.Invoke(null, new object[] { optionsBuilder, connectionString, null });
|
||||
}
|
||||
else
|
||||
{
|
||||
methodInfo?.Invoke(null, new object[] { optionsBuilder, connectionString, serverVersion, null });
|
||||
}
|
||||
|
||||
return optionsBuilder;
|
||||
}
|
||||
|
||||
private static Type GetBuilderType(IConfiguration configuration)
|
||||
{
|
||||
string provider = configuration.GetValue<string>("provider")?.ToLower();
|
||||
Type builderType;
|
||||
switch (provider)
|
||||
{
|
||||
case "mysql":
|
||||
builderType = Type.GetType("Microsoft.EntityFrameworkCore.Infrastructure.MySqlDbContextOptionsBuilder, Pomelo.EntityFrameworkCore.MySql");
|
||||
if (builderType == null)
|
||||
builderType = Type.GetType("MySql.Data.EntityFrameworkCore.Infrastructure.MySQLDbContextOptionsBuilder, MySql.Data.EntityFrameworkCore");
|
||||
break;
|
||||
case "oracle":
|
||||
builderType = Type.GetType("Oracle.EntityFrameworkCore.Infrastructure.OracleDbContextOptionsBuilder, Oracle.EntityFrameworkCore");
|
||||
break;
|
||||
case "postgres":
|
||||
case "postgresql":
|
||||
builderType = Type.GetType("Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.NpgsqlDbContextOptionsBuilder, Npgsql.EntityFrameworkCore.PostgreSQL");
|
||||
break;
|
||||
case "sqlite":
|
||||
builderType = Type.GetType("Microsoft.EntityFrameworkCore.Infrastructure.SqliteDbContextOptionsBuilder, Microsoft.EntityFrameworkCore.Sqlite");
|
||||
break;
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
builderType = Type.GetType("Microsoft.EntityFrameworkCore.Infrastructure.SqlServerDbContextOptionsBuilder, Microsoft.EntityFrameworkCore.SqlServer");
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown database provider: {provider}");
|
||||
}
|
||||
return builderType;
|
||||
}
|
||||
|
||||
private static Type GetExtensionType(IConfiguration configuration)
|
||||
{
|
||||
string provider = configuration.GetValue<string>("provider")?.ToLower();
|
||||
Type extensionType;
|
||||
switch (provider)
|
||||
{
|
||||
case "mysql":
|
||||
extensionType = Type.GetType("Microsoft.EntityFrameworkCore.MySqlDbContextOptionsBuilderExtensions, Pomelo.EntityFrameworkCore.MySql");
|
||||
if (extensionType == null)
|
||||
extensionType = Type.GetType("Microsoft.EntityFrameworkCore.MySQLDbContextOptionsBuilderExtensions, MySql.Data.EntityFrameworkCore");
|
||||
break;
|
||||
case "oracle":
|
||||
extensionType = Type.GetType("Microsoft.EntityFrameworkCore.OracleDbContextOptionsBuilderExtensions, Oracle.EntityFrameworkCore");
|
||||
break;
|
||||
case "postgres":
|
||||
case "postgresql":
|
||||
extensionType = Type.GetType("Microsoft.EntityFrameworkCore.NpgsqlDbContextOptionsBuilderExtensions, Npgsql.EntityFrameworkCore.PostgreSQL");
|
||||
break;
|
||||
case "sqlite":
|
||||
extensionType = Type.GetType("Microsoft.EntityFrameworkCore.SqliteDbContextOptionsBuilderExtensions, Microsoft.EntityFrameworkCore.Sqlite");
|
||||
break;
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
extensionType = Type.GetType("Microsoft.EntityFrameworkCore.SqlServerDbContextOptionsExtensions, Microsoft.EntityFrameworkCore.SqlServer");
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown database provider: {provider}");
|
||||
}
|
||||
return extensionType;
|
||||
}
|
||||
|
||||
private static string GetConnectionString(IConfiguration configuration, DatabaseProviderOptions options)
|
||||
{
|
||||
var cs = new List<string>();
|
||||
string provider = configuration.GetValue<string>("provider")?.ToLower();
|
||||
switch (provider)
|
||||
{
|
||||
case "mysql":
|
||||
cs.Add($"Server={configuration.GetValue<string>("Host")}");
|
||||
cs.Add($"Port={configuration.GetValue("Port", 3306)}");
|
||||
cs.Add($"Database={configuration.GetValue<string>("Name")}");
|
||||
cs.Add($"Uid={configuration.GetValue<string>("Username")}");
|
||||
cs.Add($"Password={configuration.GetValue<string>("Password")}");
|
||||
cs.Add($"Connection Timeout=15");
|
||||
break;
|
||||
case "oracle":
|
||||
cs.Add($"Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={configuration.GetValue<string>("Host")})(PORT={configuration.GetValue("Port", 1521)}))(CONNECT_DATA=(SERVICE_NAME={configuration.GetValue<string>("Name")})))");
|
||||
cs.Add($"User Id={configuration.GetValue<string>("Username")}");
|
||||
cs.Add($"Password={configuration.GetValue<string>("Password")}");
|
||||
cs.Add($"Connection Timeout=15");
|
||||
break;
|
||||
case "postgres":
|
||||
case "postgresql":
|
||||
cs.Add($"Server={configuration.GetValue<string>("Host")}");
|
||||
cs.Add($"Port={configuration.GetValue("Port", 5432)}");
|
||||
cs.Add($"Database={configuration.GetValue<string>("Name")}");
|
||||
cs.Add($"Search Path={configuration.GetValue("Schema", "public")}");
|
||||
cs.Add($"User Id={configuration.GetValue<string>("Username")}");
|
||||
cs.Add($"Password={configuration.GetValue<string>("Password")}");
|
||||
cs.Add($"Timeout=15");
|
||||
break;
|
||||
case "sqlite":
|
||||
string path = configuration.GetValue<string>("File");
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.AbsoluteBasePath))
|
||||
options.AbsoluteBasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
path = Path.Combine(options.AbsoluteBasePath, path);
|
||||
}
|
||||
cs.Add($"Data Source={path}");
|
||||
cs.Add("Foreign Keys=True");
|
||||
break;
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
cs.Add($"Server={configuration.GetValue<string>("Host")},{configuration.GetValue("Port", 1433)}");
|
||||
cs.Add($"Database={configuration.GetValue<string>("Name")}");
|
||||
if (!string.IsNullOrWhiteSpace(configuration.GetValue<string>("Username")))
|
||||
{
|
||||
cs.Add($"User Id={configuration.GetValue<string>("Username")}");
|
||||
cs.Add($"Password={configuration.GetValue<string>("Password")}");
|
||||
cs.Add("Integrated Security=False");
|
||||
}
|
||||
else
|
||||
{
|
||||
cs.Add("Integrated Security=True");
|
||||
}
|
||||
cs.Add("Connect Timeout=15");
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseProviderException($"Unknown database provider: {provider}");
|
||||
}
|
||||
return string.Join(";", cs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using AMWD.Common.EntityFrameworkCore.Attributes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
#if NET5_0_OR_GREATER
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
#endif
|
||||
|
||||
namespace AMWD.Common.EntityFrameworkCore.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for the <see cref="ModelBuilder"/> of entity framework core.
|
||||
/// </summary>
|
||||
public static class ModelBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies indices and unique constraints to the properties.
|
||||
/// </summary>
|
||||
/// <param name="builder">The database model builder.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0019", Justification = "No pattern comparison in this case due to readability.")]
|
||||
public static ModelBuilder ApplyIndexAttributes(this ModelBuilder builder)
|
||||
{
|
||||
foreach (var entityType in builder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
var indexAttribute = entityType.ClrType
|
||||
.GetProperty(property.Name)
|
||||
?.GetCustomAttribute(typeof(DatabaseIndexAttribute), false) as DatabaseIndexAttribute;
|
||||
if (indexAttribute != null)
|
||||
{
|
||||
var index = entityType.AddIndex(property);
|
||||
index.IsUnique = indexAttribute.IsUnique;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(indexAttribute.Name))
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
index.SetDatabaseName(indexAttribute.Name.Trim());
|
||||
#else
|
||||
index.SetName(indexAttribute.Name.Trim());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts all table and column names to snake_case_names.
|
||||
/// </summary>
|
||||
/// <param name="builder">The database model builder.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static ModelBuilder ApplySnakeCase(this ModelBuilder builder)
|
||||
{
|
||||
foreach (var entityType in builder.Model.GetEntityTypes())
|
||||
{
|
||||
// skip conversion when table name is explicitly set
|
||||
if ((entityType.ClrType.GetCustomAttribute(typeof(TableAttribute), false) as TableAttribute) == null)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
entityType.SetTableName(ConvertToSnakeCase(entityType.GetTableName()));
|
||||
#else
|
||||
entityType.SetTableName(ConvertToSnakeCase(entityType.GetTableName()));
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
var identifier = StoreObjectIdentifier.Table(entityType.GetTableName(), entityType.GetSchema());
|
||||
#endif
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
// skip conversion when column name is explicitly set
|
||||
if ((entityType.ClrType.GetProperty(property.Name)?.GetCustomAttribute(typeof(ColumnAttribute), false) as ColumnAttribute) == null)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
property.SetColumnName(ConvertToSnakeCase(property.GetColumnName(identifier)));
|
||||
#else
|
||||
property.SetColumnName(ConvertToSnakeCase(property.GetColumnName()));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to its snake_case equivalent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Code borrowed from Npgsql.NameTranslation.NpgsqlSnakeCaseNameTranslator.
|
||||
/// See https://github.com/npgsql/npgsql/blob/f2b2c98f45df6d2a78eec00ae867f18944d717ca/src/Npgsql/NameTranslation/NpgsqlSnakeCaseNameTranslator.cs#L76-L136.
|
||||
/// </remarks>
|
||||
/// <param name="value">The value to convert.</param>
|
||||
private static string ConvertToSnakeCase(string value)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var state = SnakeCaseState.Start;
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
if (value[i] == ' ')
|
||||
{
|
||||
if (state != SnakeCaseState.Start)
|
||||
state = SnakeCaseState.NewWord;
|
||||
}
|
||||
else if (char.IsUpper(value[i]))
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case SnakeCaseState.Upper:
|
||||
bool hasNext = (i + 1 < value.Length);
|
||||
if (i > 0 && hasNext)
|
||||
{
|
||||
char nextChar = value[i + 1];
|
||||
if (!char.IsUpper(nextChar) && nextChar != '_')
|
||||
{
|
||||
sb.Append('_');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SnakeCaseState.Lower:
|
||||
case SnakeCaseState.NewWord:
|
||||
sb.Append('_');
|
||||
break;
|
||||
}
|
||||
|
||||
sb.Append(char.ToLowerInvariant(value[i]));
|
||||
state = SnakeCaseState.Upper;
|
||||
}
|
||||
else if (value[i] == '_')
|
||||
{
|
||||
sb.Append('_');
|
||||
state = SnakeCaseState.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (state == SnakeCaseState.NewWord)
|
||||
sb.Append('_');
|
||||
|
||||
sb.Append(value[i]);
|
||||
state = SnakeCaseState.Lower;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private enum SnakeCaseState
|
||||
{
|
||||
Start,
|
||||
Lower,
|
||||
Upper,
|
||||
NewWord
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.EntityFrameworkCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the database migration.
|
||||
/// </summary>
|
||||
public class DatabaseMigrationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a logger of the type <see cref="ILogger"/>.
|
||||
/// </summary>
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the migrations table name.
|
||||
/// </summary>
|
||||
public string MigrationsTableName { get; set; } = "__migrations";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the absolute path to the migration files.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source assembly for embedded files.
|
||||
/// </summary>
|
||||
public Assembly SourceAssembly { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Microsoft.EntityFrameworkCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the database provider.
|
||||
/// </summary>
|
||||
public class DatabaseProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the absolute path to the database directory.
|
||||
/// </summary>
|
||||
public string AbsoluteBasePath { get; set; }
|
||||
}
|
||||
}
|
||||
38
AMWD.Common/AMWD.Common.csproj
Normal file
38
AMWD.Common/AMWD.Common.csproj
Normal file
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
|
||||
<AssemblyName>AMWD.Common</AssemblyName>
|
||||
<RootNamespace>AMWD.Common</RootNamespace>
|
||||
<NrtRevisionFormat>{semvertag:master:+chash}{!:-dirty}</NrtRevisionFormat>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<BuildInParallel>false</BuildInParallel>
|
||||
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<PackageId>AMWD.Common</PackageId>
|
||||
|
||||
<Product>AM.WD Common Library</Product>
|
||||
<Description>Library with classes and extensions used frequently on AM.WD projects.</Description>
|
||||
<Company>AM.WD</Company>
|
||||
<Authors>Andreas Müller</Authors>
|
||||
<Copyright>© {copyright:2020-} AM.WD</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.1" />
|
||||
<PackageReference Include="Unclassified.DeepConvert" Version="1.3.0" />
|
||||
<PackageReference Include="Unclassified.NetRevisionTask" Version="0.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
84
AMWD.Common/Extensions/CryptographyHelperExtensions.cs
Normal file
84
AMWD.Common/Extensions/CryptographyHelperExtensions.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
namespace System.Security.Cryptography
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for the <see cref="CryptographyHelper"/> class.
|
||||
/// </summary>
|
||||
public static class CryptographyHelperExtensions
|
||||
{
|
||||
#region Hashing
|
||||
|
||||
#region MD5
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5(this string str) => CryptographyHelper.Md5(str);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5(this byte[] bytes) => CryptographyHelper.Md5(bytes);
|
||||
|
||||
#endregion MD5
|
||||
|
||||
#region SHA-1
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1(this string str) => CryptographyHelper.Sha1(str);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1(this byte[] bytes) => CryptographyHelper.Sha1(bytes);
|
||||
|
||||
#endregion SHA-1
|
||||
|
||||
#region SHA-256
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256(string str) => CryptographyHelper.Sha256(str);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256(byte[] bytes) => CryptographyHelper.Sha256(bytes);
|
||||
|
||||
#endregion SHA-256
|
||||
|
||||
#region SHA-512
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512(this string str) => CryptographyHelper.Sha512(str);
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512(this byte[] bytes) => CryptographyHelper.Sha512(bytes);
|
||||
|
||||
#endregion SHA-512
|
||||
|
||||
#endregion Hashing
|
||||
}
|
||||
}
|
||||
182
AMWD.Common/Extensions/DateTimeExtensions.cs
Normal file
182
AMWD.Common/Extensions/DateTimeExtensions.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System.Text;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for date and time manipulation.
|
||||
/// </summary>
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
#region Kind
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the <see cref="DateTime.Kind"/> as UTC.
|
||||
/// </summary>
|
||||
/// <param name="dt">The <see cref="DateTime"/> instance.</param>
|
||||
/// <returns>A <see cref="DateTime"/> with correct <see cref="DateTime.Kind"/>.</returns>
|
||||
public static DateTime AsUtc(this DateTime dt)
|
||||
{
|
||||
return dt.Kind switch
|
||||
{
|
||||
DateTimeKind.Local => dt.ToUniversalTime(),
|
||||
DateTimeKind.Utc => dt,
|
||||
_ => DateTime.SpecifyKind(dt, DateTimeKind.Utc),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the <see cref="DateTime.Kind"/> as local time.
|
||||
/// </summary>
|
||||
/// <param name="dt">The <see cref="DateTime"/> instance.</param>
|
||||
/// <returns>A <see cref="DateTime"/> with correct <see cref="DateTime.Kind"/>.</returns>
|
||||
public static DateTime AsLocal(this DateTime dt)
|
||||
{
|
||||
return dt.Kind switch
|
||||
{
|
||||
DateTimeKind.Local => dt,
|
||||
DateTimeKind.Utc => dt.ToLocalTime(),
|
||||
_ => DateTime.SpecifyKind(dt, DateTimeKind.Local),
|
||||
};
|
||||
}
|
||||
|
||||
#endregion Kind
|
||||
|
||||
/// <summary>
|
||||
/// Aligns the <see cref="TimeSpan"/> to the clock.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The timespan to align.</param>
|
||||
/// <param name="offset">A specific offset to the timespan.</param>
|
||||
/// <returns>The timespan until the aligned time.</returns>
|
||||
public static TimeSpan GetAlignedInterval(this TimeSpan timeSpan, TimeSpan offset = default)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nextTime = new DateTime(now.Ticks / timeSpan.Ticks * timeSpan.Ticks) + offset;
|
||||
|
||||
if (nextTime <= now)
|
||||
nextTime += timeSpan;
|
||||
|
||||
return nextTime - now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the timespan as shortended string.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The timespan</param>
|
||||
/// <param name="withMilliseconds">A value indicating whether to show milliseconds.</param>
|
||||
/// <returns>The timespan as string.</returns>
|
||||
public static string ToShortString(this TimeSpan timeSpan, bool withMilliseconds = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (timeSpan.TotalDays >= 1)
|
||||
sb.Append(timeSpan.Days).Append("d ");
|
||||
|
||||
if (timeSpan.TotalHours >= 1)
|
||||
sb.Append(timeSpan.Hours).Append("h ");
|
||||
|
||||
if (timeSpan.TotalMinutes >= 1)
|
||||
sb.Append(timeSpan.Minutes).Append("m ");
|
||||
|
||||
sb.Append(timeSpan.Seconds).Append("s ");
|
||||
|
||||
if (withMilliseconds)
|
||||
sb.Append(timeSpan.Milliseconds).Append("ms");
|
||||
|
||||
return sb.ToString().Trim();
|
||||
}
|
||||
|
||||
#region Round DateTime
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="DateTime"/> to full seconds.
|
||||
/// </summary>
|
||||
/// <param name="dt">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
|
||||
public static DateTime RoundToSecond(this DateTime dt)
|
||||
{
|
||||
return new DateTime(RoundTicks(dt.Ticks, TimeSpan.TicksPerSecond), dt.Kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="DateTime"/> to full minutes.
|
||||
/// </summary>
|
||||
/// <param name="dt">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
public static DateTime RoundToMinute(this DateTime dt)
|
||||
{
|
||||
return new DateTime(RoundTicks(dt.Ticks, TimeSpan.TicksPerMinute), dt.Kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="DateTime"/> to full hours.
|
||||
/// </summary>
|
||||
/// <param name="dt">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
public static DateTime RoundToHour(this DateTime dt)
|
||||
{
|
||||
return new DateTime(RoundTicks(dt.Ticks, TimeSpan.TicksPerHour), dt.Kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="DateTime"/> to full days.
|
||||
/// </summary>
|
||||
/// <param name="dt">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
public static DateTime RoundToDay(this DateTime dt)
|
||||
{
|
||||
return new DateTime(RoundTicks(dt.Ticks, TimeSpan.TicksPerDay), dt.Kind);
|
||||
}
|
||||
|
||||
#endregion Round DateTime
|
||||
|
||||
#region Round TimeSpan
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="TimeSpan"/> to full seconds.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
public static TimeSpan RoundToSecond(this TimeSpan timeSpan)
|
||||
{
|
||||
return new TimeSpan(RoundTicks(timeSpan.Ticks, TimeSpan.TicksPerSecond));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="TimeSpan"/> to full minutes.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
public static TimeSpan RoundToMinute(this TimeSpan timeSpan)
|
||||
{
|
||||
return new TimeSpan(RoundTicks(timeSpan.Ticks, TimeSpan.TicksPerMinute));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="TimeSpan"/> to full hours.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
public static TimeSpan RoundToHour(this TimeSpan timeSpan)
|
||||
{
|
||||
return new TimeSpan(RoundTicks(timeSpan.Ticks, TimeSpan.TicksPerHour));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the <see cref="TimeSpan"/> to full days.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The time value to round.</param>
|
||||
/// <returns></returns>
|
||||
public static TimeSpan RoundToDay(this TimeSpan timeSpan)
|
||||
{
|
||||
return new TimeSpan(RoundTicks(timeSpan.Ticks, TimeSpan.TicksPerDay));
|
||||
}
|
||||
|
||||
#endregion Round TimeSpan
|
||||
|
||||
private static long RoundTicks(long ticks, long value)
|
||||
{
|
||||
return (ticks + value / 2) / value * value;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
AMWD.Common/Extensions/EnumExtensions.cs
Normal file
44
AMWD.Common/Extensions/EnumExtensions.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// Extend the enum values by attribute driven methods.
|
||||
/// </summary>
|
||||
public static class EnumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of specific attribute type from a enum-value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAttribute">The attribute type.</typeparam>
|
||||
/// <param name="value">The enum value.</param>
|
||||
/// <returns>The attributes or null.</returns>
|
||||
public static IEnumerable<TAttribute> GetAttributes<TAttribute>(this Enum value)
|
||||
{
|
||||
var fieldInfo = value.GetType().GetField(value.ToString());
|
||||
if (fieldInfo == null)
|
||||
return Array.Empty<TAttribute>();
|
||||
|
||||
return fieldInfo.GetCustomAttributes(typeof(TAttribute), inherit: false).Cast<TAttribute>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a specific attribute from a enum-value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAttribute">The attribute type.</typeparam>
|
||||
/// <param name="value">The enum value.</param>
|
||||
/// <returns>The attribute or null.</returns>
|
||||
public static TAttribute GetAttribute<TAttribute>(this Enum value)
|
||||
=> value.GetAttributes<TAttribute>().FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the description from <see cref="DescriptionAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value.</param>
|
||||
/// <returns>The description or the string representation of the value.</returns>
|
||||
public static string GetDescription(this Enum value)
|
||||
=> value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString();
|
||||
}
|
||||
}
|
||||
41
AMWD.Common/Extensions/ExceptionExtensions.cs
Normal file
41
AMWD.Common/Extensions/ExceptionExtensions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for exceptions.
|
||||
/// </summary>
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the message of the inner exception if exists otherwise the message of the exception itself.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception.</param>
|
||||
/// <returns>The message of the inner exception or the exception itself.</returns>
|
||||
public static string GetMessage(this Exception exception)
|
||||
=> exception.InnerException?.Message ?? exception.Message;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the message of the exception and its inner exceptions.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception.</param>
|
||||
/// <returns>The message of the <paramref name="exception"/> and all its inner exceptions.</returns>
|
||||
public static string GetRecursiveMessage(this Exception exception)
|
||||
{
|
||||
if (exception is AggregateException aggregateEx)
|
||||
{
|
||||
return aggregateEx.InnerExceptions
|
||||
.Take(3)
|
||||
.Select(ex => ex.GetRecursiveMessage())
|
||||
.Aggregate((a, b) => a + " " + b);
|
||||
}
|
||||
if (exception.InnerException != null)
|
||||
{
|
||||
string message = exception.Message;
|
||||
message = message.ReplaceEnd(" See the inner exception for details.", "");
|
||||
return message + " " + exception.InnerException.GetRecursiveMessage();
|
||||
}
|
||||
return exception.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
AMWD.Common/Extensions/JsonExtensions.cs
Normal file
158
AMWD.Common/Extensions/JsonExtensions.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Unclassified.Util;
|
||||
|
||||
namespace Newtonsoft.Json
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods to serialize and deserialize JSON values to/from objects using
|
||||
/// common naming conventions.
|
||||
/// </summary>
|
||||
public static class JsonExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Common JSON serializer settings.
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerSettings jsonSerializerSettings = new()
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Populates an instance with values deserialized from a JSON string.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the instance to populate.</typeparam>
|
||||
/// <param name="target">The instance to populate.</param>
|
||||
/// <param name="json">The JSON string to read the values from.</param>
|
||||
public static void DeserializeJson<T>(this T target, string json)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(json))
|
||||
JsonConvert.PopulateObject(json, target, jsonSerializerSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an instance to a JSON string.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the instance to serialize.</typeparam>
|
||||
/// <param name="source">The instance to serialize.</param>
|
||||
/// <param name="indented">Indicates whether the JSON string is indented to make it better readable.</param>
|
||||
/// <param name="useSingleQuotes">Indicates whether the JSON string uses single quotes instead of double quotes.</param>
|
||||
/// <param name="useCamelCase">Indicates whether the camelCase conversion should be used.</param>
|
||||
/// <param name="includeType">Indicates whether to include the instance type of <paramref name="source"/> if it is not <typeparamref name="T"/>.</param>
|
||||
/// <returns>The JSON-serialized string.</returns>
|
||||
public static string SerializeJson<T>(this T source, bool indented = false, bool useSingleQuotes = false, bool useCamelCase = true, bool includeType = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
using (var sw = new StringWriter(sb))
|
||||
using (var jw = new JsonTextWriter(sw))
|
||||
{
|
||||
if (useSingleQuotes)
|
||||
jw.QuoteChar = '\'';
|
||||
|
||||
jw.Formatting = indented ? Formatting.Indented : Formatting.None;
|
||||
var serializer = useCamelCase ? JsonSerializer.Create(jsonSerializerSettings) : JsonSerializer.CreateDefault();
|
||||
|
||||
serializer.Error += (s, a) =>
|
||||
{
|
||||
a.ErrorContext.Handled = true;
|
||||
};
|
||||
|
||||
if (includeType)
|
||||
serializer.TypeNameHandling = TypeNameHandling.Auto;
|
||||
|
||||
serializer.Serialize(jw, source, typeof(T));
|
||||
}
|
||||
return sb.ToString().Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a JSON string into a new instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the instance to deserialize.</typeparam>
|
||||
/// <param name="json">The JSON string to read the values from.</param>
|
||||
/// <returns>A new instance of <typeparamref name="T"/> with the deserialized values.</returns>
|
||||
public static T DeserializeJson<T>(this string json)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(json))
|
||||
return JsonConvert.DeserializeObject<T>(json, jsonSerializerSettings);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object into a JObject using the custom serializer settings.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to convert.</param>
|
||||
/// <returns>A JObject representing the <paramref name="obj"/>.</returns>
|
||||
public static JObject ConvertToJObject(this object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return null;
|
||||
|
||||
var serializer = JsonSerializer.Create(jsonSerializerSettings);
|
||||
return JObject.FromObject(obj, serializer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an enumerable into a JArray using the custom serializer settings.
|
||||
/// </summary>
|
||||
/// <param name="array">The enumerable to convert.</param>
|
||||
/// <returns>A JArray representing the <paramref name="array"/>.</returns>
|
||||
public static JArray ConvertToJArray(this IEnumerable array)
|
||||
{
|
||||
if (array == null)
|
||||
return null;
|
||||
|
||||
var serializer = JsonSerializer.Create(jsonSerializerSettings);
|
||||
return JArray.FromObject(array, serializer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value from the object using multiple levels.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to convert the data to.</typeparam>
|
||||
/// <param name="jObj">The object.</param>
|
||||
/// <param name="key">The key to the value.</param>
|
||||
/// <param name="defaultValue">The default value when the key was not found.</param>
|
||||
/// <param name="keySplit">The character to split the key in levels (default: colon).</param>
|
||||
/// <returns>The converted value.</returns>
|
||||
public static T GetValue<T>(this JObject jObj, string key, T defaultValue, char keySplit = ':')
|
||||
{
|
||||
if (jObj == null)
|
||||
return defaultValue;
|
||||
|
||||
string[] levels = key.Split(keySplit);
|
||||
JToken lvlObj = jObj;
|
||||
foreach (string level in levels)
|
||||
{
|
||||
if (lvlObj == null)
|
||||
return defaultValue;
|
||||
|
||||
lvlObj = lvlObj[level];
|
||||
}
|
||||
|
||||
if (lvlObj == null)
|
||||
return defaultValue;
|
||||
|
||||
if (typeof(T) == typeof(string))
|
||||
return (T)Convert.ChangeType(lvlObj, typeof(T));
|
||||
|
||||
return DeepConvert.ChangeType<T>(lvlObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value from the object using multiple levels.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to convert the data to.</typeparam>
|
||||
/// <param name="jObj">The object.</param>
|
||||
/// <param name="key">The key to the value.</param>
|
||||
/// <param name="keySplit">The character to split the key in levels (default: colon).</param>
|
||||
/// <returns>The converted value.</returns>
|
||||
public static T GetValue<T>(this JObject jObj, string key, char keySplit = ':')
|
||||
=> jObj.GetValue(key, default(T), keySplit);
|
||||
}
|
||||
}
|
||||
84
AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs
Normal file
84
AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
namespace System.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for the <see cref="ReaderWriterLockSlim"/>.
|
||||
/// </summary>
|
||||
public static class ReaderWriterLockSlimExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Acquires a read lock on a lock object that can be released with an
|
||||
/// <see cref="IDisposable"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="rwLock">The lock object.</param>
|
||||
/// <param name="timeoutMilliseconds">The number of milliseconds to wait, or -1
|
||||
/// (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> instance to release the lock.</returns>
|
||||
public static IDisposable GetReadLock(this ReaderWriterLockSlim rwLock, int timeoutMilliseconds = -1)
|
||||
{
|
||||
if (!rwLock.TryEnterReadLock(timeoutMilliseconds))
|
||||
throw new TimeoutException("The read lock could not be acquired.");
|
||||
|
||||
return new RWLockDisposable(rwLock, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires a upgradeable read lock on a lock object that can be released with an
|
||||
/// <see cref="IDisposable"/> instance. The lock can be upgraded to a write lock temporarily
|
||||
/// with <see cref="GetWriteLock"/> or until the lock is released with
|
||||
/// <see cref="ReaderWriterLockSlim.EnterWriteLock"/> alone.
|
||||
/// </summary>
|
||||
/// <param name="rwLock">The lock object.</param>
|
||||
/// <param name="timeoutMilliseconds">The number of milliseconds to wait, or -1
|
||||
/// (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> instance to release the lock. If the lock was
|
||||
/// upgraded to a write lock, that will be released as well.</returns>
|
||||
public static IDisposable GetUpgradeableReadLock(this ReaderWriterLockSlim rwLock, int timeoutMilliseconds = -1)
|
||||
{
|
||||
if (!rwLock.TryEnterUpgradeableReadLock(timeoutMilliseconds))
|
||||
throw new TimeoutException("The upgradeable read lock could not be acquired.");
|
||||
|
||||
return new RWLockDisposable(rwLock, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires a write lock on a lock object that can be released with an
|
||||
/// <see cref="IDisposable"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="rwLock">The lock object.</param>
|
||||
/// <param name="timeoutMilliseconds">The number of milliseconds to wait, or -1
|
||||
/// (<see cref="Timeout.Infinite"/>) to wait indefinitely.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> instance to release the lock.</returns>
|
||||
public static IDisposable GetWriteLock(this ReaderWriterLockSlim rwLock, int timeoutMilliseconds = -1)
|
||||
{
|
||||
if (!rwLock.TryEnterWriteLock(timeoutMilliseconds))
|
||||
throw new TimeoutException("The write lock could not be acquired.");
|
||||
|
||||
return new RWLockDisposable(rwLock, 3);
|
||||
}
|
||||
|
||||
private struct RWLockDisposable : IDisposable
|
||||
{
|
||||
private readonly ReaderWriterLockSlim rwLock;
|
||||
private int lockMode;
|
||||
|
||||
public RWLockDisposable(ReaderWriterLockSlim rwLock, int lockMode)
|
||||
{
|
||||
this.rwLock = rwLock;
|
||||
this.lockMode = lockMode;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (lockMode == 1)
|
||||
rwLock.ExitReadLock();
|
||||
if (lockMode == 2 && rwLock.IsWriteLockHeld) // Upgraded with EnterWriteLock alone
|
||||
rwLock.ExitWriteLock();
|
||||
if (lockMode == 2)
|
||||
rwLock.ExitUpgradeableReadLock();
|
||||
if (lockMode == 3)
|
||||
rwLock.ExitWriteLock();
|
||||
lockMode = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
121
AMWD.Common/Extensions/StringExtensions.cs
Normal file
121
AMWD.Common/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary>
|
||||
/// String extensions.
|
||||
/// </summary>
|
||||
public static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a hex string into a byte array.
|
||||
/// </summary>
|
||||
/// <param name="hexString">The hex encoded string.</param>
|
||||
/// <param name="delimiter">A delimiter between the bytes (e.g. for MAC address).</param>
|
||||
/// <returns>The bytes.</returns>
|
||||
public static IEnumerable<byte> HexToBytes(this string hexString, string delimiter = "")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hexString))
|
||||
yield break;
|
||||
|
||||
string str = string.IsNullOrEmpty(delimiter) ? hexString : hexString.Replace(delimiter, "");
|
||||
if (str.Length % 2 == 1)
|
||||
yield break;
|
||||
|
||||
for (int i = 0; i < str.Length; i += 2)
|
||||
yield return Convert.ToByte(str.Substring(i, 2), 16);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte collection into a hex string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <param name="delimiter">A delimiter to set between the bytes (e.g. for MAC address).</param>
|
||||
/// <returns>The hex encoded string.</returns>
|
||||
public static string BytesToHex(this IEnumerable<byte> bytes, string delimiter = "")
|
||||
{
|
||||
if (bytes?.Any() != true)
|
||||
return null;
|
||||
|
||||
return string.Join(delimiter, bytes.Select(b => b.ToString("x2")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string to the hexadecimal system (base 16).
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="encoding"></param>
|
||||
/// <returns></returns>
|
||||
public static string HexEncode(this string str, Encoding encoding = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return str;
|
||||
|
||||
return (encoding ?? Encoding.Default).GetBytes(str).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a string from the hexadecimal system (base 16).
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="encoding"></param>
|
||||
/// <returns></returns>
|
||||
public static string HexDecode(this string str, Encoding encoding = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return str;
|
||||
|
||||
return (encoding ?? Encoding.Default).GetString(str.HexToBytes().ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string to base64.
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="encoding"></param>
|
||||
/// <returns></returns>
|
||||
public static string Base64Encode(this string str, Encoding encoding = null)
|
||||
=> Convert.ToBase64String((encoding ?? Encoding.Default).GetBytes(str));
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a string from base64.
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="encoding"></param>
|
||||
/// <returns></returns>
|
||||
public static string Base64Decode(this string str, Encoding encoding = null)
|
||||
=> (encoding ?? Encoding.Default).GetString(Convert.FromBase64String(str));
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the search substring with the replacement when it was found at the beginning of the string.
|
||||
/// </summary>
|
||||
/// <param name="str">The string.</param>
|
||||
/// <param name="search">The searched substring.</param>
|
||||
/// <param name="replacement">The replacement.</param>
|
||||
/// <returns></returns>
|
||||
public static string ReplaceStart(this string str, string search, string replacement)
|
||||
{
|
||||
if (str.StartsWith(search))
|
||||
return replacement + str.Substring(search.Length);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the search substring with the replacement when it was found at the end of the string.
|
||||
/// </summary>
|
||||
/// <param name="str">The string.</param>
|
||||
/// <param name="search">The searched substring.</param>
|
||||
/// <param name="replacement">The replacement.</param>
|
||||
/// <returns></returns>
|
||||
public static string ReplaceEnd(this string str, string search, string replacement)
|
||||
{
|
||||
if (str.EndsWith(search))
|
||||
return str.Substring(0, str.Length - search.Length) + replacement;
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
486
AMWD.Common/Utilities/CryptographyHelper.cs
Normal file
486
AMWD.Common/Utilities/CryptographyHelper.cs
Normal file
@@ -0,0 +1,486 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Security.Cryptography
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides cryptographic functions ready-to-use.
|
||||
/// </summary>
|
||||
public class CryptographyHelper
|
||||
{
|
||||
private static readonly int saltLength = 8;
|
||||
|
||||
private readonly string masterKeyFile;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CryptographyHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="keyFile">The (absolute) path to the crypto key file. On <c>null</c> the file 'crypto.key' at the executing assembly location will be used.</param>
|
||||
public CryptographyHelper(string keyFile = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyFile))
|
||||
keyFile = "crypto.key";
|
||||
|
||||
if (!Path.IsPathRooted(keyFile))
|
||||
{
|
||||
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
keyFile = Path.Combine(dir, keyFile);
|
||||
}
|
||||
masterKeyFile = keyFile;
|
||||
|
||||
string pw = File.Exists(masterKeyFile) ? File.ReadAllText(masterKeyFile) : null;
|
||||
if (string.IsNullOrWhiteSpace(pw))
|
||||
File.WriteAllText(masterKeyFile, GetRandomString(64));
|
||||
}
|
||||
|
||||
#region Instance methods
|
||||
|
||||
#region AES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption (optional).</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public byte[] DecryptAes(byte[] cipher, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return AesDecrypt(cipher, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption (optional).</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public byte[] EncryptAes(byte[] plain, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return AesEncrypt(plain, password);
|
||||
}
|
||||
|
||||
#endregion AES
|
||||
|
||||
#region Triple DES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption (optional).</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public byte[] DecryptTripleDes(byte[] cipher, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return TripleDesDecrypt(cipher, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption (optional).</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public byte[] EncryptTripleDes(byte[] plain, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return TripleDesEncrypt(plain, password);
|
||||
}
|
||||
|
||||
#endregion Triple DES
|
||||
|
||||
#endregion Instance methods
|
||||
|
||||
#region Static methods
|
||||
|
||||
#region Encryption
|
||||
|
||||
#region AES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption.</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public static byte[] AesDecrypt(byte[] cipher, string password)
|
||||
{
|
||||
byte[] salt = new byte[saltLength];
|
||||
Array.Copy(cipher, salt, saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var aes = Aes.Create();
|
||||
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = gen.GetBytes(aes.KeySize / 8);
|
||||
aes.IV = gen.GetBytes(aes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write);
|
||||
|
||||
cs.Write(cipher, saltLength, cipher.Length - saltLength);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption.</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public static byte[] AesEncrypt(byte[] plain, string password)
|
||||
{
|
||||
byte[] salt = GetRandomBytes(saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var aes = Aes.Create();
|
||||
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = gen.GetBytes(aes.KeySize / 8);
|
||||
aes.IV = gen.GetBytes(aes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write);
|
||||
|
||||
ms.Write(salt, 0, salt.Length);
|
||||
cs.Write(plain, 0, plain.Length);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
#endregion AES
|
||||
|
||||
#region Triple DES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption.</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public static byte[] TripleDesDecrypt(byte[] cipher, string password)
|
||||
{
|
||||
byte[] salt = new byte[saltLength];
|
||||
Array.Copy(cipher, salt, saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var tdes = TripleDES.Create();
|
||||
|
||||
tdes.Mode = CipherMode.CBC;
|
||||
tdes.Padding = PaddingMode.PKCS7;
|
||||
tdes.Key = gen.GetBytes(tdes.KeySize / 8);
|
||||
tdes.IV = gen.GetBytes(tdes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, tdes.CreateDecryptor(), CryptoStreamMode.Write);
|
||||
|
||||
cs.Write(cipher, saltLength, cipher.Length - saltLength);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption.</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public static byte[] TripleDesEncrypt(byte[] plain, string password)
|
||||
{
|
||||
byte[] salt = GetRandomBytes(saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var tdes = TripleDES.Create();
|
||||
|
||||
tdes.Mode = CipherMode.CBC;
|
||||
tdes.Padding = PaddingMode.PKCS7;
|
||||
tdes.Key = gen.GetBytes(tdes.KeySize / 8);
|
||||
tdes.IV = gen.GetBytes(tdes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, tdes.CreateEncryptor(), CryptoStreamMode.Write);
|
||||
|
||||
ms.Write(salt, 0, salt.Length);
|
||||
cs.Write(plain, 0, plain.Length);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
#endregion Triple DES
|
||||
|
||||
#endregion Encryption
|
||||
|
||||
#region Hashing
|
||||
|
||||
#region MD5
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5(string str)
|
||||
{
|
||||
return Md5(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5File(string fileName)
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return md5.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5(byte[] bytes)
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
return md5.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion MD5
|
||||
|
||||
#region SHA-1
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1(string str)
|
||||
{
|
||||
return Sha1(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1File(string fileName)
|
||||
{
|
||||
using var sha1 = SHA1.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return sha1.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1(byte[] bytes)
|
||||
{
|
||||
using var sha1 = SHA1.Create();
|
||||
return sha1.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion SHA-1
|
||||
|
||||
#region SHA-256
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256(string str)
|
||||
{
|
||||
return Sha256(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256File(string fileName)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return sha256.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256(byte[] bytes)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
return sha256.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion SHA-256
|
||||
|
||||
#region SHA-512
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512(string str)
|
||||
{
|
||||
return Sha512(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512File(string fileName)
|
||||
{
|
||||
using var sha512 = SHA512.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return sha512.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512(byte[] bytes)
|
||||
{
|
||||
using var sha512 = SHA512.Create();
|
||||
return sha512.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion SHA-512
|
||||
|
||||
#endregion Hashing
|
||||
|
||||
#region Random
|
||||
|
||||
/// <summary>
|
||||
/// Generates an array with random (non-zero) bytes.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of bytes to generate.</param>
|
||||
/// <returns></returns>
|
||||
public static byte[] GetRandomBytes(int count)
|
||||
{
|
||||
using var gen = RandomNumberGenerator.Create();
|
||||
byte[] bytes = new byte[count];
|
||||
gen.GetNonZeroBytes(bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string with random characters.
|
||||
/// </summary>
|
||||
/// <param name="length">The length of the string to generate.</param>
|
||||
/// <param name="pool">The characters to use (Default: [a-zA-Z0-9]).</param>
|
||||
/// <returns></returns>
|
||||
public static string GetRandomString(int length, string pool = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pool))
|
||||
pool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
|
||||
var sb = new StringBuilder(length);
|
||||
int multiply = sizeof(int) / sizeof(byte);
|
||||
int len = length * multiply;
|
||||
byte[] bytes = GetRandomBytes(len);
|
||||
for (int i = 0; i < bytes.Length; i += multiply)
|
||||
{
|
||||
uint number = BitConverter.ToUInt32(bytes, i);
|
||||
sb.Append(pool[(int)(number % pool.Length)]);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion Random
|
||||
|
||||
#region Probing security
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two strings are equal in constant time. This method does not stop
|
||||
/// early if a difference was detected, unless the length differs.
|
||||
/// </summary>
|
||||
/// <param name="a">The first string.</param>
|
||||
/// <param name="b">The second string.</param>
|
||||
/// <returns>true, if both strings are equal; otherwise, false.</returns>
|
||||
public static bool SecureEquals(string a, string b)
|
||||
{
|
||||
if ((a == null) != (b == null))
|
||||
return false;
|
||||
if (a.Length != b.Length)
|
||||
return false;
|
||||
|
||||
int differentBits = 0;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
differentBits |= a[i] ^ b[i];
|
||||
}
|
||||
return differentBits == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two byte arrays are equal in constant time. This method does not stop
|
||||
/// early if a difference was detected, unless the length differs.
|
||||
/// </summary>
|
||||
/// <param name="a">The first array.</param>
|
||||
/// <param name="b">The second array.</param>
|
||||
/// <returns>true, if both arrays are equal; otherwise, false.</returns>
|
||||
public static bool SecureEquals(byte[] a, byte[] b)
|
||||
{
|
||||
if ((a == null) != (b == null))
|
||||
return false;
|
||||
if (a.Length != b.Length)
|
||||
return false;
|
||||
|
||||
int differentBits = 0;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
differentBits |= a[i] ^ b[i];
|
||||
}
|
||||
return differentBits == 0;
|
||||
}
|
||||
|
||||
#endregion Probing security
|
||||
|
||||
#endregion Static methods
|
||||
}
|
||||
}
|
||||
590
AMWD.Common/Utilities/DelayedTask.cs
Normal file
590
AMWD.Common/Utilities/DelayedTask.cs
Normal file
@@ -0,0 +1,590 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements an awaitable task that runs after a specified delay. The delay can be reset
|
||||
/// before and after the task has run. By resetting the delay, the task can be executed multiple
|
||||
/// times. The scheduled or executing or last executed task can be awaited, until the delay is
|
||||
/// reset. After that, the next execution can be awaited.
|
||||
/// </summary>
|
||||
public class DelayedTask
|
||||
{
|
||||
#region Data
|
||||
|
||||
/// <summary>
|
||||
/// The synchronisation object.
|
||||
/// </summary>
|
||||
protected readonly object syncObj = new();
|
||||
|
||||
/// <summary>
|
||||
/// The exception handler.
|
||||
/// </summary>
|
||||
protected Action<Exception> exceptionHandler;
|
||||
|
||||
private Timer timer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the timer is running and an execution is scheduled. This
|
||||
/// is mutually exclusive to <see cref="IsRunning"/>.
|
||||
/// </summary>
|
||||
public bool IsWaitingToRun { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the action is currently running. This is mutually
|
||||
/// exclusive to <see cref="IsWaitingToRun"/>.
|
||||
/// </summary>
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the action shall be executed again after the currently ongoing
|
||||
/// execution has completed.
|
||||
/// </summary>
|
||||
private bool nextRunPending;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the <see cref="Task"/> for the <see cref="GetAwaiter"/> method.
|
||||
/// </summary>
|
||||
protected TaskCompletionSourceWrapper tcs;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action to execute.
|
||||
/// </summary>
|
||||
protected Action Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delay to wait before executing the action.
|
||||
/// </summary>
|
||||
public TimeSpan Delay { get; protected set; }
|
||||
|
||||
#endregion Data
|
||||
|
||||
#region Static methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new task instance that executes the specified action after the delay, but does
|
||||
/// not start it yet. Multiple executions are allowed when calling <see cref="Reset"/> after
|
||||
/// the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTask Create(Action action, TimeSpan delay)
|
||||
{
|
||||
return new DelayedTask { Action = action, Delay = delay };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new task instance that executes the specified action after the delay, but does
|
||||
/// not start it yet. Multiple executions are allowed when calling <see cref="Reset"/> after
|
||||
/// the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTaskWithResult<TResult> Create<TResult>(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return DelayedTaskWithResult<TResult>.Create(action, delay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified action after the delay. Multiple executions are allowed when
|
||||
/// calling <see cref="Reset"/> after the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTask Run(Action action, TimeSpan delay)
|
||||
{
|
||||
return new DelayedTask { Action = action, Delay = delay }.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified action after the delay. Multiple executions are allowed when
|
||||
/// calling <see cref="Reset"/> after the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTaskWithResult<TResult> Run<TResult>(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return DelayedTaskWithResult<TResult>.Run(action, delay);
|
||||
}
|
||||
|
||||
#endregion Static methods
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelayedTask"/> class.
|
||||
/// </summary>
|
||||
protected DelayedTask()
|
||||
{
|
||||
tcs = CreateTcs();
|
||||
SetLastResult(tcs);
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Public instance methods
|
||||
|
||||
/// <summary>
|
||||
/// Resets the delay and restarts the timer. If an execution is currently pending, it is
|
||||
/// postponed until the full delay has elapsed again. If no execution is pending, the action
|
||||
/// will be executed again after the delay.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
if (!IsWaitingToRun && !IsRunning)
|
||||
{
|
||||
// Let callers wait for the next execution
|
||||
tcs = CreateTcs();
|
||||
}
|
||||
IsWaitingToRun = true;
|
||||
if (timer != null)
|
||||
{
|
||||
timer.Change(Delay, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the delay. Any pending executions are cleared. If the action was pending but not
|
||||
/// yet executing, this task is cancelled. If the action was not pending or is already
|
||||
/// executing, this task will be completed successfully after the action has completed.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
TaskCompletionSourceWrapper localTcs = null;
|
||||
lock (syncObj)
|
||||
{
|
||||
IsWaitingToRun = false;
|
||||
nextRunPending = false;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
if (!IsRunning)
|
||||
{
|
||||
localTcs = tcs;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the task (as cancelled) so that nobody needs to wait for an execution that
|
||||
// isn't currently scheduled
|
||||
localTcs?.TrySetCanceled();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a pending execution immediately, not waiting for the timer to elapse.
|
||||
/// </summary>
|
||||
/// <returns>true, if an execution was started; otherwise, false.</returns>
|
||||
/// <remarks>
|
||||
/// A new execution is only started if one is currently waiting to run, or already running.
|
||||
/// In the former case, the execution is scheduled immediately with the timer; in the latter
|
||||
/// case, it is scheduled for when the currently running execution has completed. If an
|
||||
/// execution has been started (the method returned true), it can be awaited normally.
|
||||
/// </remarks>
|
||||
public bool ExecutePending()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
if (!IsWaitingToRun && !IsRunning)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IsWaitingToRun = true;
|
||||
if (timer != null)
|
||||
{
|
||||
timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an awaiter used to await this <see cref="DelayedTask"/>.
|
||||
/// </summary>
|
||||
/// <returns>An awaiter instance.</returns>
|
||||
public TaskAwaiter GetAwaiter()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
return tcs.Task.GetAwaiter();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="System.Threading.Tasks.Task"/> that represents the current awaitable
|
||||
/// operation.
|
||||
/// </summary>
|
||||
public Task Task
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="DelayedTask"/> to
|
||||
/// <see cref="System.Threading.Tasks.Task"/>.
|
||||
/// </summary>
|
||||
/// <param name="delayedTask">The <see cref="DelayedTask"/> instance to cast.</param>
|
||||
/// <returns>A <see cref="System.Threading.Tasks.Task"/> that represents the current
|
||||
/// awaitable operation.</returns>
|
||||
public static implicit operator Task(DelayedTask delayedTask) => delayedTask.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exception of the last execution. If the action has not yet thrown any
|
||||
/// exceptions, this will return null.
|
||||
/// </summary>
|
||||
public Exception Exception => Task.Exception;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an unhandled exception handler to this <see cref="DelayedTask"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="exceptionHandler">The action that handles an exception.</param>
|
||||
/// <returns>The current instance.</returns>
|
||||
public DelayedTask WithExceptionHandler(Action<Exception> exceptionHandler)
|
||||
{
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion Public instance methods
|
||||
|
||||
#region Non-public methods
|
||||
|
||||
/// <summary>
|
||||
/// Starts the current instance after creating it.
|
||||
/// </summary>
|
||||
/// <returns>The current instance.</returns>
|
||||
protected DelayedTask Start()
|
||||
{
|
||||
tcs = CreateTcs();
|
||||
IsWaitingToRun = true;
|
||||
timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="TaskCompletionSourceWrapper"/> instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual TaskCompletionSourceWrapper CreateTcs()
|
||||
{
|
||||
return new TaskCompletionSourceWrapper<object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the timer has elapsed.
|
||||
/// </summary>
|
||||
/// <param name="state">Unused.</param>
|
||||
protected void OnTimerCallback(object state)
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
if (!IsWaitingToRun)
|
||||
{
|
||||
// Already cancelled, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
IsWaitingToRun = false;
|
||||
if (IsRunning)
|
||||
{
|
||||
// Currently running, remember and do nothing for now
|
||||
nextRunPending = true;
|
||||
return;
|
||||
}
|
||||
IsRunning = true;
|
||||
}
|
||||
|
||||
// Run as long as there are pending executions and the instance has not been disposed of
|
||||
bool runAgain;
|
||||
TaskCompletionSourceWrapper localTcs = null;
|
||||
Exception exception = null;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
lock (syncObj)
|
||||
{
|
||||
runAgain = false;
|
||||
IsRunning = false;
|
||||
nextRunPending = false;
|
||||
localTcs = tcs;
|
||||
if (!IsWaitingToRun)
|
||||
{
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
exceptionHandler?.Invoke(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
runAgain = nextRunPending;
|
||||
IsRunning = runAgain;
|
||||
nextRunPending = false;
|
||||
if (!runAgain)
|
||||
{
|
||||
if (!IsWaitingToRun)
|
||||
{
|
||||
localTcs = tcs;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (runAgain);
|
||||
|
||||
// Unblock waiters if not already waiting for the next execution.
|
||||
// This task can be awaited again after the Reset method has been called.
|
||||
if (exception != null)
|
||||
localTcs?.TrySetException(exception);
|
||||
else
|
||||
SetLastResult(localTcs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the action of the task.
|
||||
/// </summary>
|
||||
protected virtual void Run()
|
||||
{
|
||||
Action();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="TaskCompletionSourceWrapper"/> result from the last action.
|
||||
/// </summary>
|
||||
/// <param name="tcs">The <see cref="TaskCompletionSourceWrapper"/> to set the result of.</param>
|
||||
protected virtual void SetLastResult(TaskCompletionSourceWrapper tcs)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<object>)tcs;
|
||||
myTcs?.TrySetResult(default);
|
||||
}
|
||||
|
||||
#endregion Non-public methods
|
||||
|
||||
#region Internal TaskCompletionSourceWrapper classes
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="TaskCompletionSource{TResult}"/> instance in a non-generic way to
|
||||
/// allow sharing it in the non-generic base class.
|
||||
/// </summary>
|
||||
protected abstract class TaskCompletionSourceWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Task{TResult}"/> of the <see cref="TaskCompletionSource{TResult}"/>.
|
||||
/// </summary>
|
||||
public abstract Task Task { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Faulted"/> state and binds it to a specified exception.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to bind to this <see cref="Task{TResult}"/>.</param>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetException(Exception)"/>
|
||||
public abstract void TrySetException(Exception exception);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Canceled"/> state.
|
||||
/// </summary>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetCanceled()"/>
|
||||
public abstract void TrySetCanceled();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TaskCompletionSourceWrapper"/> that provides a result value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the result value.</typeparam>
|
||||
protected class TaskCompletionSourceWrapper<TResult> : TaskCompletionSourceWrapper
|
||||
{
|
||||
private readonly TaskCompletionSource<TResult> tcs;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Task{TResult}"/> of the <see cref="TaskCompletionSource{TResult}"/>.
|
||||
/// </summary>
|
||||
public override Task Task => tcs.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TaskCompletionSourceWrapper{TResult}"/> class.
|
||||
/// </summary>
|
||||
public TaskCompletionSourceWrapper()
|
||||
{
|
||||
tcs = new TaskCompletionSource<TResult>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.RanToCompletion"/> state.
|
||||
/// </summary>
|
||||
/// <param name="result">The result value to bind to this <see cref="Task{TResult}"/>.</param>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetResult(TResult)"/>
|
||||
public void TrySetResult(TResult result)
|
||||
{
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Faulted"/> state and binds it to a specified exception.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to bind to this <see cref="Task{TResult}"/>.</param>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetException(Exception)"/>
|
||||
public override void TrySetException(Exception exception)
|
||||
{
|
||||
tcs.TrySetException(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Canceled"/> state.
|
||||
/// </summary>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetCanceled()"/>
|
||||
public override void TrySetCanceled()
|
||||
{
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Internal TaskCompletionSourceWrapper classes
|
||||
}
|
||||
|
||||
#region Generic derived classes
|
||||
|
||||
/// <summary>
|
||||
/// Implements an awaitable task that runs after a specified delay. The delay can be reset
|
||||
/// before and after the task has run.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the return value of the action.</typeparam>
|
||||
public class DelayedTaskWithResult<TResult> : DelayedTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of the last execution of the action.
|
||||
/// </summary>
|
||||
protected TResult lastResult;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action to execute.
|
||||
/// </summary>
|
||||
protected new Func<TResult> Action { get; set; }
|
||||
|
||||
internal static DelayedTaskWithResult<TResult> Create(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return new DelayedTaskWithResult<TResult> { Action = action, Delay = delay };
|
||||
}
|
||||
|
||||
internal static DelayedTaskWithResult<TResult> Run(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return (DelayedTaskWithResult<TResult>)new DelayedTaskWithResult<TResult> { Action = action, Delay = delay }.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="DelayedTask.TaskCompletionSourceWrapper"/> instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override TaskCompletionSourceWrapper CreateTcs()
|
||||
{
|
||||
return new TaskCompletionSourceWrapper<TResult>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the action of the task.
|
||||
/// </summary>
|
||||
protected override void Run()
|
||||
{
|
||||
lastResult = Action();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="DelayedTask.TaskCompletionSourceWrapper"/> result from the last action.
|
||||
/// </summary>
|
||||
/// <param name="tcs">The <see cref="DelayedTask.TaskCompletionSourceWrapper"/> to set the result of.</param>
|
||||
protected override void SetLastResult(TaskCompletionSourceWrapper tcs)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
||||
myTcs?.TrySetResult(lastResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an awaiter used to await this <see cref="DelayedTask"/>.
|
||||
/// </summary>
|
||||
/// <returns>An awaiter instance.</returns>
|
||||
public new TaskAwaiter<TResult> GetAwaiter()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
||||
var myTask = (Task<TResult>)myTcs.Task;
|
||||
return myTask.GetAwaiter();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Task{TResult}"/> that represents the current awaitable operation.
|
||||
/// </summary>
|
||||
public new Task<TResult> Task
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
||||
var myTask = (Task<TResult>)myTcs.Task;
|
||||
return myTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="DelayedTaskWithResult{TResult}"/> to
|
||||
/// <see cref="Task{TResult}"/>.
|
||||
/// </summary>
|
||||
/// <param name="delayedTask">The <see cref="DelayedTaskWithResult{TResult}"/> instance to cast.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> that represents the current awaitable operation.</returns>
|
||||
public static implicit operator Task<TResult>(DelayedTaskWithResult<TResult> delayedTask)
|
||||
{
|
||||
return delayedTask.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an unhandled exception handler to this <see cref="DelayedTask"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="exceptionHandler">The action that handles an exception.</param>
|
||||
/// <returns>The current instance.</returns>
|
||||
public new DelayedTaskWithResult<TResult> WithExceptionHandler(Action<Exception> exceptionHandler)
|
||||
{
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Generic derived classes
|
||||
}
|
||||
81
AMWD.Common/Utilities/NetworkHelper.cs
Normal file
81
AMWD.Common/Utilities/NetworkHelper.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides some network utils.
|
||||
/// </summary>
|
||||
public static class NetworkHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to resolve a <paramref name="hostname"/> into an <see cref="IPAddress"/> to connect to.
|
||||
/// </summary>
|
||||
/// <param name="hostname">The hostname to resolve.</param>
|
||||
/// <param name="addressFamily">An address family to use (available: <see cref="AddressFamily.InterNetwork"/> and <see cref="AddressFamily.InterNetworkV6"/>).</param>
|
||||
/// <param name="fallback">The fallback ip address when resolving failed.</param>
|
||||
/// <returns>The resolved <see cref="IPAddress"/> to connect to or <paramref name="fallback"/> value.</returns>
|
||||
public static IPAddress ResolveHost(string hostname, AddressFamily addressFamily = AddressFamily.Unspecified, IPAddress fallback = null)
|
||||
{
|
||||
if (IPAddress.TryParse(hostname, out var ipAddress))
|
||||
{
|
||||
if (ipAddress.AddressFamily != AddressFamily.InterNetwork && ipAddress.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
return fallback;
|
||||
|
||||
if (addressFamily != AddressFamily.Unspecified && ipAddress.AddressFamily != addressFamily)
|
||||
return fallback;
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
return Dns.GetHostAddresses(hostname)
|
||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
.Where(a => addressFamily == AddressFamily.Unspecified || a.AddressFamily == addressFamily)
|
||||
.OrderBy(a => a.AddressFamily)
|
||||
.FirstOrDefault() ?? fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve a <paramref name="iface"/> into an <see cref="IPAddress"/> to bind (listen) on.
|
||||
/// </summary>
|
||||
/// <param name="iface">The interface name to resolve.</param>
|
||||
/// <param name="addressFamily">An address family to use (available: <see cref="AddressFamily.InterNetwork"/> and <see cref="AddressFamily.InterNetworkV6"/>).</param>
|
||||
/// <param name="fallback">The fallback ip address when resolving failed.</param>
|
||||
/// <returns>The resolved <see cref="IPAddress"/> to bind on or <paramref name="fallback"/> value.</returns>
|
||||
public static IPAddress ResolveInterface(string iface, AddressFamily addressFamily = AddressFamily.Unspecified, IPAddress fallback = null)
|
||||
{
|
||||
if (IPAddress.TryParse(iface, out var ipAddress))
|
||||
{
|
||||
if (ipAddress.AddressFamily != AddressFamily.InterNetwork && ipAddress.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
return fallback;
|
||||
|
||||
if (addressFamily != AddressFamily.Unspecified && ipAddress.AddressFamily != addressFamily)
|
||||
return fallback;
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Dns.GetHostAddresses(iface)
|
||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
.Where(a => addressFamily == AddressFamily.Unspecified || a.AddressFamily == addressFamily)
|
||||
.OrderBy(a => a.AddressFamily)
|
||||
.FirstOrDefault() ?? fallback;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.Name.Equals(iface, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses.Select(ai => ai.Address))
|
||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
.Where(a => addressFamily == AddressFamily.Unspecified || a.AddressFamily == addressFamily)
|
||||
.OrderBy(a => a.AddressFamily)
|
||||
.FirstOrDefault() ?? fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
337
CodeMaid.config
Normal file
337
CodeMaid.config
Normal file
@@ -0,0 +1,337 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="SteveCadwallader.CodeMaid.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<SteveCadwallader.CodeMaid.Properties.Settings>
|
||||
<setting name="Cleaning_AutoCleanupOnFileSave" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_ExcludeT4GeneratedCode" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_ExclusionExpression" serializeAs="String">
|
||||
<value>.*\.Designer\.cs||.*\.resx||packages.config||.*\.min\.js||.*\.min\.css</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeCPlusPlus" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeCSS" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeCSharp" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeFSharp" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeHTML" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeJSON" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeJavaScript" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeLESS" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludePHP" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeSCSS" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeTypeScript" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeVB" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeXAML" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_IncludeXML" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterClasses" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterDelegates"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterEndRegionTags"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterEnumerations"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterEvents" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterFieldsMultiLine"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterInterfaces"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterMethods" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterNamespaces"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterPropertiesMultiLine"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterPropertiesSingleLine"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterRegionTags"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterStructs" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingAfterUsingStatementBlocks"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeCaseStatements"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeClasses"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeDelegates"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeEndRegionTags"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeEnumerations"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeEvents" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeFieldsMultiLine"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeInterfaces"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeMethods"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeNamespaces"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforePropertiesMultiLine"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforePropertiesSingleLine"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeRegionTags"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeSingleLineComments"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeStructs"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBeforeUsingStatementBlocks"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankLinePaddingBetweenPropertiesMultiLineAccessors"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertBlankSpaceBeforeSelfClosingAngleBrackets"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertEndOfFileTrailingNewLine" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnClasses"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnDelegates"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnEnumerations"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnEvents"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnFields"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnInterfaces"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnMethods"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnProperties"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_InsertExplicitAccessModifiersOnStructs"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankLinesAfterAttributes" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankLinesAfterOpeningBrace" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankLinesAtBottom" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankLinesAtTop" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankLinesBeforeClosingBrace" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankLinesBeforeClosingTags" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankLinesBetweenChainedStatements"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveBlankSpacesBeforeClosingAngleBrackets"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveEndOfFileTrailingNewLine" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveEndOfLineWhitespace" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveMultipleConsecutiveBlankLines"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RemoveRegions" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RunVisualStudioFormatDocumentCommand"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RunVisualStudioRemoveUnusedUsingStatements"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_RunVisualStudioSortUsingStatements" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_SkipRemoveUnusedUsingStatementsDuringAutoCleanupOnSave"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_UpdateAccessorsToBothBeSingleLineOrMultiLine"
|
||||
serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_UpdateEndRegionDirectives" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_UpdateSingleLineMethods" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Cleaning_UsingStatementsToReinsertWhenRemovedExpression"
|
||||
serializeAs="String">
|
||||
<value>
|
||||
</value>
|
||||
</setting>
|
||||
<setting name="Collapsing_CollapseSolutionWhenOpened" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentRunDuringCleanup" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentSkipWrapOnLastWord" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentWrapColumn" serializeAs="String">
|
||||
<value>100</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentXmlAlignParamTags" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentXmlKeepTagsTogether" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentXmlSpaceSingleTags" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentXmlSpaceTags" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentXmlSplitAllTags" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentXmlSplitSummaryTagToMultipleLines"
|
||||
serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Formatting_CommentXmlValueIndent" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
<setting name="General_ShowStartPageOnSolutionClose" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Progressing_HideBuildProgressOnBuildStop" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Progressing_ShowBuildProgressOnBuildStart" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
</SteveCadwallader.CodeMaid.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
45
Common.sln
Normal file
45
Common.sln
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31729.503
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Common", "AMWD.Common\AMWD.Common.csproj", "{F512C474-B670-4E47-911E-7C0674AA8E7E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Common.AspNetCore", "AMWD.Common.AspNetCore\AMWD.Common.AspNetCore.csproj", "{725F40C9-8172-487F-B3D0-D7E38B4DB197}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Common.EntityFrameworkCore", "AMWD.Common.EntityFrameworkCore\AMWD.Common.EntityFrameworkCore.csproj", "{7091CECF-C981-4FB9-9CC6-91C4E65A6356}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
.gitlab-ci.yml = .gitlab-ci.yml
|
||||
CodeMaid.config = CodeMaid.config
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{F512C474-B670-4E47-911E-7C0674AA8E7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F512C474-B670-4E47-911E-7C0674AA8E7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F512C474-B670-4E47-911E-7C0674AA8E7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F512C474-B670-4E47-911E-7C0674AA8E7E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{725F40C9-8172-487F-B3D0-D7E38B4DB197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{725F40C9-8172-487F-B3D0-D7E38B4DB197}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{725F40C9-8172-487F-B3D0-D7E38B4DB197}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{725F40C9-8172-487F-B3D0-D7E38B4DB197}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7091CECF-C981-4FB9-9CC6-91C4E65A6356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7091CECF-C981-4FB9-9CC6-91C4E65A6356}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7091CECF-C981-4FB9-9CC6-91C4E65A6356}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7091CECF-C981-4FB9-9CC6-91C4E65A6356}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {961E8DF8-DDF5-4D10-A510-CE409E9962AC}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
30
build.bat
Normal file
30
build.bat
Normal file
@@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
set Configuration=Release
|
||||
|
||||
cd "%~dp0"
|
||||
cd "AMWD.Common"
|
||||
rmdir /S /Q bin
|
||||
dotnet build -c %Configuration% --nologo --no-incremental
|
||||
|
||||
cd "%~dp0"
|
||||
cd "AMWD.Common.AspNetCore"
|
||||
rmdir /S /Q bin
|
||||
dotnet build -c %Configuration% --nologo --no-incremental
|
||||
|
||||
cd "%~dp0"
|
||||
cd "AMWD.Common.EntityFrameworkCore"
|
||||
rmdir /S /Q bin
|
||||
dotnet build -c %Configuration% --nologo --no-incremental
|
||||
|
||||
cd "%~dp0"
|
||||
rmdir /S /Q build
|
||||
mkdir build
|
||||
|
||||
move AMWD.Common\bin\%Configuration%\*.nupkg build
|
||||
move AMWD.Common\bin\%Configuration%\*.snupkg build
|
||||
|
||||
move AMWD.Common.AspNetCore\bin\%Configuration%\*.nupkg build
|
||||
move AMWD.Common.AspNetCore\bin\%Configuration%\*.snupkg build
|
||||
|
||||
move AMWD.Common.EntityFrameworkCore\bin\%Configuration%\*.nupkg build
|
||||
move AMWD.Common.EntityFrameworkCore\bin\%Configuration%\*.snupkg build
|
||||
29
build.sh
Normal file
29
build.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONFIGURATION=Release
|
||||
|
||||
pushd AMWD.Common
|
||||
rm -rf bin
|
||||
dotnet build -c ${CONFIGURATION} --nologo --no-incremental
|
||||
popd
|
||||
|
||||
pushd AMWD.Common.AspNetCore
|
||||
rm -rf bin
|
||||
dotnet build -c ${CONFIGURATION} --nologo --no-incremental
|
||||
popd
|
||||
|
||||
pushd AMWD.Common.EntityFrameworkCore
|
||||
rm -rf bin
|
||||
dotnet build -c ${CONFIGURATION} --nologo --no-incremental
|
||||
popd
|
||||
|
||||
rm -rf build
|
||||
mkdir build
|
||||
mv AMWD.Common/bin/${CONFIGURATION}/*.nupkg build
|
||||
mv AMWD.Common/bin/${CONFIGURATION}/*.snupkg build
|
||||
|
||||
mv AMWD.Common.AspNetCore/bin/${CONFIGURATION}/*.nupkg build
|
||||
mv AMWD.Common.AspNetCore/bin/${CONFIGURATION}/*.snupkg build
|
||||
|
||||
mv AMWD.Common.EntityFrameworkCore/bin/${CONFIGURATION}/*.nupkg build
|
||||
mv AMWD.Common.EntityFrameworkCore/bin/${CONFIGURATION}/*.snupkg build
|
||||
Reference in New Issue
Block a user