First commit. Include junk from template to remove
This commit is contained in:
362
.azure/bicep/main.bicep
Normal file
362
.azure/bicep/main.bicep
Normal file
@@ -0,0 +1,362 @@
|
||||
@description('The location into which your Azure resources should be deployed.')
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('Select the type of environment you want to provision. Allowed values are Production, Staging, and Development.')
|
||||
@allowed([
|
||||
'Production'
|
||||
'Staging'
|
||||
'Development'
|
||||
])
|
||||
param environmentName string
|
||||
|
||||
@description('A unique suffix to add to resource names that need to be globally unique.')
|
||||
@maxLength(13)
|
||||
param resourceNameSuffix string = uniqueString(resourceGroup().id)
|
||||
|
||||
@description('The administrator login username for the SQL server.')
|
||||
param sqlAdministratorUsername string
|
||||
|
||||
@secure()
|
||||
@description('The administrator login password for the SQL server.')
|
||||
param sqlAdministratorPassword string
|
||||
|
||||
@description('The name of the project.')
|
||||
param projectName string
|
||||
|
||||
// Define the environment configuration map.
|
||||
var environmentConfigurationMap = {
|
||||
Production: {
|
||||
environmentAbbreviation: 'prd'
|
||||
appServicePlan: {
|
||||
sku: {
|
||||
name: 'S1'
|
||||
capacity: 1
|
||||
}
|
||||
}
|
||||
sqlDatabase: {
|
||||
sku: {
|
||||
name: 'Standard'
|
||||
tier: 'Standard'
|
||||
}
|
||||
}
|
||||
}
|
||||
Staging: {
|
||||
environmentAbbreviation: 'stg'
|
||||
appServicePlan: {
|
||||
sku: {
|
||||
name: 'B1'
|
||||
}
|
||||
}
|
||||
sqlDatabase: {
|
||||
sku: {
|
||||
name: 'Standard'
|
||||
tier: 'Standard'
|
||||
}
|
||||
}
|
||||
}
|
||||
Development: {
|
||||
environmentAbbreviation: 'dev'
|
||||
appServicePlan: {
|
||||
sku: {
|
||||
name: 'B1'
|
||||
}
|
||||
}
|
||||
sqlDatabase: {
|
||||
sku: {
|
||||
name: 'Standard'
|
||||
tier: 'Standard'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define the names for resources.
|
||||
var environmentAbbreviation = environmentConfigurationMap[environmentName].environmentAbbreviation
|
||||
var keyVaultName = 'kv-${projectName}-${environmentAbbreviation}'
|
||||
var appServiceAppName = 'as-${projectName}-${resourceNameSuffix}-${environmentAbbreviation}'
|
||||
var appServicePlanName = 'plan-${projectName}-${environmentAbbreviation}'
|
||||
var logAnalyticsWorkspaceName = 'log-${projectName}-${environmentAbbreviation}'
|
||||
var applicationInsightsName = 'appi-${projectName}-${environmentAbbreviation}'
|
||||
var sqlServerName = 'sql-${projectName}-${resourceNameSuffix}-${environmentAbbreviation}'
|
||||
var sqlDatabaseName = '${projectName}-${environmentAbbreviation}'
|
||||
|
||||
// Define the SKUs for each component based on the environment type.
|
||||
var appServicePlanSku = environmentConfigurationMap[environmentName].appServicePlan.sku
|
||||
var sqlDatabaseSku = environmentConfigurationMap[environmentName].sqlDatabase.sku
|
||||
|
||||
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-10-01' = {
|
||||
name: logAnalyticsWorkspaceName
|
||||
location: location
|
||||
properties: {
|
||||
sku: {
|
||||
name: 'PerGB2018'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' = {
|
||||
name: keyVaultName
|
||||
location: location
|
||||
properties: {
|
||||
enabledForTemplateDeployment: true
|
||||
tenantId: subscription().tenantId
|
||||
accessPolicies: []
|
||||
sku: {
|
||||
name: 'standard'
|
||||
family: 'A'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource keyVault_ConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
|
||||
parent: keyVault
|
||||
name: 'ConnectionStrings--DefaultConnection'
|
||||
properties: {
|
||||
value: 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Initial Catalog=${sqlDatabaseName};Persist Security Info=False;User ID=${sqlAdministratorUsername};Password=${sqlAdministratorPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;'
|
||||
}
|
||||
dependsOn: [
|
||||
sqlDatabase
|
||||
]
|
||||
}
|
||||
|
||||
resource keyVault_DiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
|
||||
scope: keyVault
|
||||
name: 'keyVaultDiagnosticSettings'
|
||||
properties: {
|
||||
workspaceId: logAnalyticsWorkspace.id
|
||||
logs: [
|
||||
{
|
||||
category: 'AuditEvent'
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
metrics: [
|
||||
{
|
||||
category: 'AllMetrics'
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource appServicePlan 'Microsoft.Web/serverfarms@2021-01-15' = {
|
||||
name: appServicePlanName
|
||||
location: location
|
||||
sku: appServicePlanSku
|
||||
}
|
||||
|
||||
resource appServicePlan_DiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
|
||||
scope: appServicePlan
|
||||
name: 'appServicePlanDiagnosticSettings'
|
||||
properties: {
|
||||
workspaceId: logAnalyticsWorkspace.id
|
||||
metrics: [
|
||||
{
|
||||
category: 'AllMetrics'
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource appServiceApp 'Microsoft.Web/sites@2021-01-15' = {
|
||||
name: appServiceAppName
|
||||
location: location
|
||||
identity: {
|
||||
type: 'SystemAssigned'
|
||||
}
|
||||
properties: {
|
||||
serverFarmId: appServicePlan.id
|
||||
httpsOnly: true
|
||||
siteConfig: {
|
||||
healthCheckPath: '/health'
|
||||
netFrameworkVersion: 'v7.0'
|
||||
appSettings: [
|
||||
{
|
||||
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
||||
value: applicationInsights.properties.ConnectionString
|
||||
}
|
||||
{
|
||||
name: 'KeyVaultUri'
|
||||
value: keyVault.properties.vaultUri
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource appServiceApp_DiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
|
||||
scope: appServiceApp
|
||||
name: 'appServiceAppDiagnosticSettings'
|
||||
properties: {
|
||||
workspaceId: logAnalyticsWorkspace.id
|
||||
logs: [
|
||||
{
|
||||
category: 'AppServiceHTTPLogs'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'AppServiceConsoleLogs'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'AppServiceAppLogs'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'AppServiceAuditLogs'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'AppServiceIPSecAuditLogs'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'AppServicePlatformLogs'
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
metrics: [
|
||||
{
|
||||
category: 'AllMetrics'
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource keyVault_AccessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = {
|
||||
parent: keyVault
|
||||
name: 'add'
|
||||
properties: {
|
||||
accessPolicies: [
|
||||
{
|
||||
tenantId: appServiceApp.identity.tenantId
|
||||
objectId: appServiceApp.identity.principalId
|
||||
permissions: {
|
||||
keys: [
|
||||
'Get'
|
||||
]
|
||||
secrets: [
|
||||
'Get'
|
||||
'List'
|
||||
|
||||
]
|
||||
certificates: [
|
||||
'Get'
|
||||
'List'
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
|
||||
name: applicationInsightsName
|
||||
location: location
|
||||
kind: 'web'
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
WorkspaceResourceId: logAnalyticsWorkspace.id
|
||||
}
|
||||
}
|
||||
|
||||
resource sqlServer 'Microsoft.Sql/servers@2021-02-01-preview' = {
|
||||
name: sqlServerName
|
||||
location: location
|
||||
properties: {
|
||||
administratorLogin: sqlAdministratorUsername
|
||||
administratorLoginPassword: sqlAdministratorPassword
|
||||
}
|
||||
}
|
||||
|
||||
resource sqlDatabase 'Microsoft.Sql/servers/databases@2021-02-01-preview' = {
|
||||
parent: sqlServer
|
||||
name: sqlDatabaseName
|
||||
location: location
|
||||
sku: sqlDatabaseSku
|
||||
}
|
||||
|
||||
resource sqlServer_AuditingSettings 'Microsoft.Sql/servers/auditingSettings@2021-11-01-preview' = {
|
||||
parent: sqlServer
|
||||
name: 'default'
|
||||
properties: {
|
||||
state: 'Enabled'
|
||||
isAzureMonitorTargetEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
resource sqlServer_FirewallRule 'Microsoft.Sql/servers/firewallRules@2021-02-01-preview' = {
|
||||
parent: sqlServer
|
||||
name: 'AllowAllWindowsAzureIps'
|
||||
properties: {
|
||||
endIpAddress: '0.0.0.0'
|
||||
startIpAddress: '0.0.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
resource sqlDatabase_DiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
|
||||
scope: sqlDatabase
|
||||
name: 'sqlDatabaseDiagnosticSettings'
|
||||
properties: {
|
||||
workspaceId: logAnalyticsWorkspace.id
|
||||
logs: [
|
||||
{
|
||||
category: 'SQLInsights'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'AutomaticTuning'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'QueryStoreRuntimeStatistics'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'QueryStoreWaitStatistics'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'Errors'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'DatabaseWaitStatistics'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'Timeouts'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'Blocks'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'Deadlocks'
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
metrics: [
|
||||
{
|
||||
category: 'Basic'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'InstanceAndAppAdvanced'
|
||||
enabled: true
|
||||
}
|
||||
{
|
||||
category: 'WorkloadManagement'
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
output appServiceAppName string = appServiceApp.name
|
||||
output appServiceAppHostName string = appServiceApp.properties.defaultHostName
|
||||
output sqlServerFullyQualifiedDomainName string = sqlServer.properties.fullyQualifiedDomainName
|
||||
output sqlDatabaseName string = sqlDatabase.name
|
||||
382
.editorconfig
Normal file
382
.editorconfig
Normal file
@@ -0,0 +1,382 @@
|
||||
root = true
|
||||
|
||||
# All files
|
||||
[*]
|
||||
indent_style = space
|
||||
|
||||
# Xml files
|
||||
[*.{xml,csproj,props,targets,ruleset,nuspec,resx}]
|
||||
indent_size = 2
|
||||
|
||||
# Javascript files
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
# Json files
|
||||
[*.{json,config,nswag}]
|
||||
indent_size = 2
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
|
||||
#### Core EditorConfig Options ####
|
||||
|
||||
# Indentation and spacing
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
# New line preferences
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
#### .NET Coding Conventions ####
|
||||
[*.{cs,vb}]
|
||||
|
||||
# Organize usings
|
||||
dotnet_separate_import_directive_groups = false
|
||||
dotnet_sort_system_directives_first = true
|
||||
file_header_template = unset
|
||||
|
||||
# this. and Me. preferences
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
|
||||
# Language keywords vs BCL types preferences
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||
dotnet_style_predefined_type_for_member_access = true:silent
|
||||
|
||||
# Parentheses preferences
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
|
||||
|
||||
# Modifier preferences
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
dotnet_style_prefer_auto_properties = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
|
||||
# Field preferences
|
||||
dotnet_style_readonly_field = true:warning
|
||||
|
||||
# Parameter preferences
|
||||
dotnet_code_quality_unused_parameters = all:suggestion
|
||||
|
||||
# Suppression preferences
|
||||
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
|
||||
#### C# Coding Conventions ####
|
||||
[*.cs]
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = false:silent
|
||||
csharp_style_var_for_built_in_types = false:silent
|
||||
csharp_style_var_when_type_is_apparent = false:silent
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_prefer_not_pattern = true:suggestion
|
||||
csharp_style_prefer_pattern_matching = true:silent
|
||||
csharp_style_prefer_switch_expression = true:suggestion
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_local_function = true:warning
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
|
||||
# 'using' directive preferences
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
|
||||
#### C# Formatting Rules ####
|
||||
|
||||
# New line preferences
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
# 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 = false
|
||||
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
|
||||
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
|
||||
#### Naming styles ####
|
||||
[*.{cs,vb}]
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
|
||||
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
|
||||
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
|
||||
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
|
||||
|
||||
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
|
||||
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
|
||||
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
|
||||
|
||||
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
|
||||
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
|
||||
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
|
||||
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
|
||||
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
|
||||
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
|
||||
|
||||
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
|
||||
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
|
||||
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
|
||||
|
||||
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
|
||||
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
|
||||
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
|
||||
|
||||
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
|
||||
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
|
||||
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
|
||||
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
|
||||
|
||||
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
|
||||
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
|
||||
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
|
||||
|
||||
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
|
||||
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
|
||||
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
|
||||
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
|
||||
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
|
||||
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
|
||||
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interfaces.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interfaces.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.enums.applicable_kinds = enum
|
||||
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.enums.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.events.applicable_kinds = event
|
||||
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.events.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.methods.applicable_kinds = method
|
||||
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.methods.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.properties.applicable_kinds = property
|
||||
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.properties.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.public_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
|
||||
dotnet_naming_symbols.public_fields.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.private_fields.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.private_static_fields.required_modifiers = static
|
||||
|
||||
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
|
||||
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
|
||||
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.type_parameters.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
|
||||
|
||||
dotnet_naming_symbols.local_variables.applicable_kinds = local
|
||||
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
|
||||
dotnet_naming_symbols.local_variables.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.local_constants.applicable_kinds = local
|
||||
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
|
||||
dotnet_naming_symbols.local_constants.required_modifiers = const
|
||||
|
||||
dotnet_naming_symbols.parameters.applicable_kinds = parameter
|
||||
dotnet_naming_symbols.parameters.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.parameters.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
|
||||
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
|
||||
|
||||
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
|
||||
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
|
||||
|
||||
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
|
||||
|
||||
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
|
||||
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.local_functions.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.pascalcase.required_prefix =
|
||||
dotnet_naming_style.pascalcase.required_suffix =
|
||||
dotnet_naming_style.pascalcase.word_separator =
|
||||
dotnet_naming_style.pascalcase.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.ipascalcase.required_prefix = I
|
||||
dotnet_naming_style.ipascalcase.required_suffix =
|
||||
dotnet_naming_style.ipascalcase.word_separator =
|
||||
dotnet_naming_style.ipascalcase.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.tpascalcase.required_prefix = T
|
||||
dotnet_naming_style.tpascalcase.required_suffix =
|
||||
dotnet_naming_style.tpascalcase.word_separator =
|
||||
dotnet_naming_style.tpascalcase.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style._camelcase.required_prefix = _
|
||||
dotnet_naming_style._camelcase.required_suffix =
|
||||
dotnet_naming_style._camelcase.word_separator =
|
||||
dotnet_naming_style._camelcase.capitalization = camel_case
|
||||
|
||||
dotnet_naming_style.camelcase.required_prefix =
|
||||
dotnet_naming_style.camelcase.required_suffix =
|
||||
dotnet_naming_style.camelcase.word_separator =
|
||||
dotnet_naming_style.camelcase.capitalization = camel_case
|
||||
|
||||
dotnet_naming_style.s_camelcase.required_prefix = s_
|
||||
dotnet_naming_style.s_camelcase.required_suffix =
|
||||
dotnet_naming_style.s_camelcase.word_separator =
|
||||
dotnet_naming_style.s_camelcase.capitalization = camel_case
|
||||
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: JasonTaylorDev # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
# patreon: # Replace with a single Patreon username
|
||||
# open_collective: # Replace with a single Open Collective username
|
||||
# ko_fi: # Replace with a single Ko-fi username
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: # Replace with a single Liberapay username
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# otechie: # Replace with a single Otechie username
|
||||
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
82
.github/workflows/build.yml
vendored
Normal file
82
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- '.scripts/**'
|
||||
- .gitignore
|
||||
- CODE_OF_CONDUCT.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
build-artifacts:
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout code
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
|
||||
- name: Restore solution
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build --no-restore --configuration Release
|
||||
|
||||
- name: Test solution
|
||||
run: dotnet test --no-build --configuration Release --filter "FullyQualifiedName!~AcceptanceTests"
|
||||
|
||||
- name: Publish website
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
run: |
|
||||
dotnet publish --configuration Release --runtime win-x86 --self-contained --output ./publish
|
||||
cd publish
|
||||
zip -r ./publish.zip .
|
||||
working-directory: ./src/Web/
|
||||
|
||||
- name: Upload website artifact (website)
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: website
|
||||
path: ./src/Web/publish/publish.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create EF Core migrations bundle
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
run: |
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install dotnet-ef
|
||||
dotnet ef migrations bundle --configuration Release -p ./src/Infrastructure/ -s ./src/Web/ -o efbundle.exe
|
||||
zip -r ./efbundle.zip efbundle.exe
|
||||
env:
|
||||
SkipNSwag: True
|
||||
|
||||
- name: Upload EF Core migrations bundle artifact (efbundle)
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: efbundle
|
||||
path: ./efbundle.zip
|
||||
if-no-files-found: error
|
||||
42
.github/workflows/cicd.yml
vendored
Normal file
42
.github/workflows/cicd.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: CICD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- .gitignore
|
||||
- CODE_OF_CONDUCT.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
build-artifacts: true
|
||||
|
||||
deploy-development:
|
||||
uses: ./.github/workflows/deploy.yml
|
||||
secrets: inherit
|
||||
needs: [ build ]
|
||||
with:
|
||||
environmentName: Development
|
||||
|
||||
deploy-staging:
|
||||
uses: ./.github/workflows/deploy.yml
|
||||
secrets: inherit
|
||||
needs: [ deploy-development ]
|
||||
with:
|
||||
environmentName: Staging
|
||||
|
||||
deploy-production:
|
||||
uses: ./.github/workflows/deploy.yml
|
||||
secrets: inherit
|
||||
needs: [ deploy-staging ]
|
||||
with:
|
||||
environmentName: Production
|
||||
107
.github/workflows/deploy.yml
vendored
Normal file
107
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environmentName:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environmentName }}
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout code
|
||||
|
||||
- uses: azure/login@v1
|
||||
name: Login to Azure
|
||||
with:
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- if: inputs.environmentName == 'Development'
|
||||
uses: azure/arm-deploy@v1
|
||||
name: Run preflight validation
|
||||
with:
|
||||
deploymentName: ${{ github.run_number }}
|
||||
resourceGroupName: ${{ vars.AZURE_RESOURCE_GROUP_NAME }}
|
||||
template: ./.azure/bicep/main.bicep
|
||||
parameters: >
|
||||
environmentName=${{ inputs.environmentName }}
|
||||
sqlAdministratorUsername=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }}
|
||||
sqlAdministratorPassword=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }}
|
||||
projectName=${{ vars.PROJECT_NAME }}
|
||||
deploymentMode: Validate
|
||||
|
||||
- if: inputs.environmentName != 'Development'
|
||||
uses: azure/arm-deploy@v1
|
||||
name: Run what-if
|
||||
with:
|
||||
failOnStdErr: false
|
||||
resourceGroupName: ${{ vars.AZURE_RESOURCE_GROUP_NAME }}
|
||||
template: ./.azure/bicep/main.bicep
|
||||
parameters: >
|
||||
environmentName=${{ inputs.environmentName }}
|
||||
sqlAdministratorUsername=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }}
|
||||
sqlAdministratorPassword=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }}
|
||||
projectName=${{ vars.PROJECT_NAME }}
|
||||
additionalArguments: --what-if
|
||||
|
||||
deploy:
|
||||
needs: [ validate ]
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environmentName }}
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout code
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
name: Download artifacts
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
|
||||
- uses: azure/login@v1
|
||||
name: Login to Azure
|
||||
with:
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- uses: azure/arm-deploy@v1
|
||||
id: deploy
|
||||
name: Deploy infrastructure
|
||||
with:
|
||||
failOnStdErr: false
|
||||
deploymentName: ${{ github.run_number }}
|
||||
resourceGroupName: ${{ vars.AZURE_RESOURCE_GROUP_NAME }}
|
||||
template: ./.azure/bicep/main.bicep
|
||||
parameters: >
|
||||
environmentName=${{ inputs.environmentName }}
|
||||
sqlAdministratorUsername=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }}
|
||||
sqlAdministratorPassword=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }}
|
||||
projectName=${{ vars.PROJECT_NAME }}
|
||||
|
||||
- name: Initialise database
|
||||
run: |
|
||||
unzip -o ./efbundle/efbundle.zip
|
||||
echo '{ "ConnectionStrings": { "DefaultConnection": "" } }' > appsettings.json
|
||||
./efbundle.exe --connection "Server=${{ steps.deploy.outputs.sqlServerFullyQualifiedDomainName }};Initial Catalog=${{ steps.deploy.outputs.sqlDatabaseName }};Persist Security Info=False;User ID=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }};Password=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" --verbose
|
||||
|
||||
- uses: azure/webapps-deploy@v2
|
||||
name: Deploy website
|
||||
with:
|
||||
app-name: ${{ steps.deploy.outputs.appServiceAppName }}
|
||||
package: website/publish.zip
|
||||
480
.gitignore
vendored
Normal file
480
.gitignore
vendored
Normal file
@@ -0,0 +1,480 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# Mac bundle stuff
|
||||
*.dmg
|
||||
*.app
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# 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
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# 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
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
29
.scripts/checks.ps1
Normal file
29
.scripts/checks.ps1
Normal file
@@ -0,0 +1,29 @@
|
||||
# Check if Azure CLI is installed
|
||||
$azPath = (Get-Command az -ErrorAction SilentlyContinue).Source
|
||||
if (-not $azPath) {
|
||||
throw "Azure CLI (az) is not installed. Please install it and try again."
|
||||
}
|
||||
|
||||
# Check if Azure CLI is authenticated
|
||||
az account show --output none
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Azure CLI (az) is not authenticated. Please authenticate with Azure CLI and try again."
|
||||
}
|
||||
|
||||
# Check if GitHub CLI is installed
|
||||
$ghPath = (Get-Command gh -ErrorAction SilentlyContinue).Source
|
||||
if (-not $ghPath) {
|
||||
throw "GitHub CLI (gh) is not installed. Please install it and try again."
|
||||
}
|
||||
|
||||
# Check if GitHub CLI is authenticated
|
||||
gh auth status | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "GitHub CLI (gh) is not authenticated. Please authenticate with GitHub CLI and try again."
|
||||
}
|
||||
|
||||
# Check if Git repo is initialised
|
||||
git status | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "The Git repository has not been initialised. Please create a new GitHub repository and try again."
|
||||
}
|
||||
78
.scripts/cleanup.ps1
Normal file
78
.scripts/cleanup.ps1
Normal file
@@ -0,0 +1,78 @@
|
||||
Param(
|
||||
[String]$ProjectName
|
||||
)
|
||||
|
||||
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$checksScript = Join-Path $scriptRoot "checks.ps1"
|
||||
|
||||
try {
|
||||
. $checksScript
|
||||
} catch {
|
||||
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||
Write-Host "Setup script terminated due to the checks failure." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$MissingParameterValues = $false
|
||||
|
||||
if (-not $ProjectName) {
|
||||
$ProjectName = $(gh repo view --json name -q '.name' 2> $null)
|
||||
if (-not $ProjectName) { $MissingParameterValues = $true }
|
||||
}
|
||||
|
||||
$ScriptParameters = @{
|
||||
"ProjectName" = $ProjectName
|
||||
}
|
||||
|
||||
Write-Host
|
||||
Write-Host "This script performs cleanup operations to delete resource groups, Azure AD applications, and purge deleted key vaults associated with a specific project hosted on GitHub. It searches for resources based on the project name and performs the necessary deletion and purging actions. The script leverages the Azure CLI to interact with Azure resources. It aims to facilitate the cleanup process and remove unnecessary resources from your Azure environment."
|
||||
Write-Host
|
||||
Write-Host "Parameters:" -ForegroundColor Green
|
||||
$ScriptParameters | Format-Table -AutoSize
|
||||
Write-Host
|
||||
|
||||
if ($MissingParameterValues) {
|
||||
Write-Host "Script execution cancelled. Missing parameter values." -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Warning: This script will perform cleanup operations, including deleting resource groups, Azure AD applications, and purging deleted key vaults starting with the project name '$ProjectName'. Make sure you understand the consequences and have verified the project name before proceeding." -ForegroundColor Red
|
||||
Write-Host
|
||||
Write-Host "Disclaimer: Use this script at your own risk. The author and contributors are not responsible for any loss of data or unintended consequences resulting from running this script." -ForegroundColor Yellow
|
||||
Write-Host
|
||||
|
||||
$confirmation = Read-Host "Do you want to continue? (y/N)"
|
||||
|
||||
if ($confirmation -ne "y") {
|
||||
Write-Host "Script execution cancelled." -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "🔍 Searching for Resource Groups..."
|
||||
$resourceGroups = az group list --query "[?starts_with(name, '$ProjectName')].name" --output tsv
|
||||
|
||||
foreach ($rg in $resourceGroups) {
|
||||
Write-Host "🔥 Deleting: $rg"
|
||||
az group delete --name $rg --yes > $null 2>&1
|
||||
}
|
||||
|
||||
Write-Host "🔍 Searching for Azure AD Applications..."
|
||||
$appRegistrations = az ad app list --display-name $ProjectName --query "[].{Name:displayName, AppId:appId}" --output json | ConvertFrom-Json
|
||||
|
||||
foreach ($appRegistration in $appRegistrations) {
|
||||
$appName = $appRegistration.Name
|
||||
$appId = $appRegistration.AppId
|
||||
|
||||
Write-Host "🔥 Deleting: $appName"
|
||||
az ad app delete --id $appId > $null 2>&1
|
||||
}
|
||||
|
||||
Write-Host "🔍 Searching for Deleted Key Vaults..."
|
||||
$deletedKeyVaults = az keyvault list-deleted --query "[?starts_with(name, 'kv-$ProjectName')].name" --output tsv
|
||||
|
||||
foreach ($vaultName in $deletedKeyVaults) {
|
||||
Write-Host "🔥 Purging: $vaultName"
|
||||
az keyvault purge --name $vaultName > $null 2>&1
|
||||
}
|
||||
|
||||
Write-Host "✅ Done"
|
||||
5
.scripts/environments.json
Normal file
5
.scripts/environments.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"Dev": "Development",
|
||||
"Stg": "Staging",
|
||||
"Prd": "Production"
|
||||
}
|
||||
210
.scripts/setup.ps1
Normal file
210
.scripts/setup.ps1
Normal file
@@ -0,0 +1,210 @@
|
||||
Param(
|
||||
[String]$GitHubOrganisationName,
|
||||
[String]$GitHubRepositoryName,
|
||||
[String]$AzureLocation,
|
||||
[String]$AzureSubscriptionId,
|
||||
[String]$AzureTenantId,
|
||||
[ValidateLength(4, 17)]
|
||||
[String]$ProjectName,
|
||||
[String]$AzureSqlLogin = "SqlAdmin"
|
||||
)
|
||||
|
||||
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$checksScript = Join-Path $scriptRoot "checks.ps1"
|
||||
$environmentsFile = Join-Path $scriptRoot "environments.json"
|
||||
|
||||
try {
|
||||
. $checksScript
|
||||
} catch {
|
||||
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||
Write-Host "Setup script terminated due to the checks failure." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$MissingParameterValues = $false
|
||||
|
||||
if (-not $GitHubOrganisationName) {
|
||||
$ownerJson = gh repo view --json owner 2>$null | ConvertFrom-Json
|
||||
if ($ownerJson -and $ownerJson.owner -and $ownerJson.owner.login) {
|
||||
$GitHubOrganisationName = $ownerJson.owner.login
|
||||
}
|
||||
else {
|
||||
$MissingParameterValues = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $GitHubRepositoryName) {
|
||||
$GitHubRepositoryName = $(gh repo view --json name -q '.name' 2> $null)
|
||||
if (-not $GitHubRepositoryName) { $MissingParameterValues = $true }
|
||||
}
|
||||
|
||||
if (-not $AzureLocation) {
|
||||
$AzureLocation = "australiaeast"
|
||||
}
|
||||
|
||||
if (-not $AzureSubscriptionId) {
|
||||
$AzureSubscriptionId = $(az account show --query id --output tsv 2> $null)
|
||||
if (-not $AzureSubscriptionId) { $MissingParameterValues = $true }
|
||||
}
|
||||
|
||||
if (-not $AzureTenantId) {
|
||||
$AzureTenantId = $(az account show --query tenantId --output tsv 2> $null)
|
||||
if (-not $AzureTenantId) { $MissingParameterValues = $true }
|
||||
}
|
||||
|
||||
if (-not $ProjectName) {
|
||||
if ($GitHubRepositoryName) {
|
||||
$ProjectName = $GitHubRepositoryName
|
||||
}
|
||||
|
||||
if (-not $ProjectName) { $MissingParameterValues = $true }
|
||||
}
|
||||
|
||||
$repoUrl = "https://github.com/$GitHubOrganisationName/$GitHubRepositoryName"
|
||||
|
||||
$environments = Get-Content -Raw -Path $environmentsFile | ConvertFrom-Json
|
||||
|
||||
$ParametersTableData = @{
|
||||
"AzureLocation" = $AzureLocation
|
||||
"AzureSubscriptionID" = $AzureSubscriptionId
|
||||
"AzureTenantID" = $AzureTenantId
|
||||
"GitHubOrganisationName" = $GitHubOrganisationName
|
||||
"GitHubRepositoryName" = $GitHubRepositoryName
|
||||
"ProjectName" = $ProjectName
|
||||
"AzureSqlLogin" = $AzureSqlLogin
|
||||
}
|
||||
|
||||
Write-Host
|
||||
Write-Host "This script automates the setup of environments, resources, and credentials for a project hosted on GitHub and deployed to Azure. It creates workload identities in Azure AD, sets up resource groups, and configures environment-specific variables and secrets in the GitHub repository. The script leverages the Azure CLI, GitHub CLI, and GitHub APIs to perform these tasks. It aims to streamline the process of setting up and configuring development, staging, and production environments for the project."
|
||||
Write-Host
|
||||
Write-Host "Parameters:" -ForegroundColor Green
|
||||
$ParametersTableData | Format-Table -AutoSize
|
||||
|
||||
if ($MissingParameterValues) {
|
||||
Write-Host "Script execution cancelled. Missing parameter values." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$EnvironmentTableData = foreach ($environment in $environments.PSObject.Properties) {
|
||||
[PSCustomObject]@{
|
||||
Abbreviation = $environment.Name
|
||||
Name = $environment.Value
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Environments:" -ForegroundColor Green
|
||||
$EnvironmentTableData | Select-Object Name, Abbreviation | Format-Table -AutoSize
|
||||
Write-Host
|
||||
|
||||
Write-Host "Warning: Running this script will perform various operations in your GitHub repository and Azure subscription. Ensure that you have the necessary permissions and understand the consequences. " -ForegroundColor Red
|
||||
Write-Host
|
||||
Write-Host "Disclaimer: Use this script at your own risk. The author and contributors are not responsible for any loss of data or unintended consequences resulting from running this script." -ForegroundColor Yellow
|
||||
Write-Host
|
||||
|
||||
$confirmation = Read-Host "Do you want to continue? (y/N)"
|
||||
|
||||
if ($confirmation -ne "y") {
|
||||
Write-Host "Script execution cancelled." -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host
|
||||
|
||||
function CreateWorkloadIdentity {
|
||||
param (
|
||||
$environmentAbbr,
|
||||
$environmentName
|
||||
)
|
||||
|
||||
# Create Azure AD Application Registration
|
||||
$applicationRegistrationDetails=$(az ad app create --display-name "$ProjectName$environmentAbbr") | ConvertFrom-Json
|
||||
|
||||
# Create federated credentials
|
||||
$credential = @{
|
||||
name="$ProjectName$environmentName";
|
||||
issuer="https://token.actions.githubusercontent.com";
|
||||
subject="repo:${GitHubOrganisationName}/${GitHubRepositoryName}:environment:$environmentName";
|
||||
audiences=@("api://AzureADTokenExchange")
|
||||
} | ConvertTo-Json
|
||||
|
||||
$credential | az ad app federated-credential create --id $applicationRegistrationDetails.id --parameters "@-" | Out-Null
|
||||
|
||||
$credential = @{
|
||||
name="$ProjectName";
|
||||
issuer="https://token.actions.githubusercontent.com";
|
||||
subject="repo:${GitHubOrganisationName}/${GitHubRepositoryName}:ref:refs/heads/main";
|
||||
audiences=@("api://AzureADTokenExchange")
|
||||
} | ConvertTo-Json
|
||||
|
||||
$credential | az ad app federated-credential create --id $applicationRegistrationDetails.id --parameters "@-" | Out-Null
|
||||
|
||||
return $applicationRegistrationDetails.appId
|
||||
}
|
||||
|
||||
function CreateResourceGroup {
|
||||
param (
|
||||
$environmentAbbr,
|
||||
$appId
|
||||
)
|
||||
|
||||
$resourceGroupId = $(az group create --name "$ProjectName$environmentAbbr" --location $AzureLocation --query id --output tsv)
|
||||
az ad sp create --id $appId
|
||||
az role assignment create --assignee $appId --role Contributor --scope $resourceGroupId
|
||||
}
|
||||
|
||||
function CreateEnvironment {
|
||||
param (
|
||||
$environmentName
|
||||
)
|
||||
|
||||
$token = gh auth token
|
||||
$header = @{"Authorization" = "token $token" }
|
||||
$contentType = "application/json"
|
||||
|
||||
$uri = "https://api.github.com/repos/$GitHubOrganisationName/$GitHubRepositoryName/environments/$environmentName"
|
||||
Invoke-WebRequest -Method PUT -Header $header -ContentType $contentType -Uri $uri
|
||||
}
|
||||
|
||||
function GenerateRandomPassword {
|
||||
param (
|
||||
[int]$Length = 16
|
||||
)
|
||||
|
||||
$ValidChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#^_-+=?<>|~".ToCharArray()
|
||||
$Password = -join ((Get-Random -Count $Length -InputObject $ValidChars) | Get-Random -Count $Length)
|
||||
|
||||
return $Password
|
||||
}
|
||||
|
||||
function SetVariables() {
|
||||
gh variable set AZURE_TENANT_ID --body $AzureTenantId --repo $repoUrl
|
||||
gh variable set AZURE_SUBSCRIPTION_ID --body $AzureSubscriptionId --repo $repoUrl
|
||||
gh variable set PROJECT_NAME --body $ProjectName --repo $repoUrl
|
||||
}
|
||||
|
||||
function SetEnvironmentVariablesAndSecrets {
|
||||
param(
|
||||
$environmentAbbr,
|
||||
$environmentName,
|
||||
$appId
|
||||
)
|
||||
|
||||
gh variable set AZURE_CLIENT_ID --body "$appId" --env $environmentName --repo $repoUrl
|
||||
gh variable set AZURE_RESOURCE_GROUP_NAME --body "$ProjectName$environmentAbbr" --env $environmentName --repo $repoUrl
|
||||
gh variable set AZURE_SQL_ADMINISTRATOR_USERNAME --body "$AzureSqlLogin" --env $environmentName --repo $repoUrl
|
||||
gh secret set AZURE_SQL_ADMINISTRATOR_PASSWORD --body (GenerateRandomPassword) --env $environmentName --repo $repoUrl
|
||||
}
|
||||
|
||||
SetVariables
|
||||
|
||||
foreach ($environment in $environments.PSObject.Properties) {
|
||||
$environmentAbbr = $environment.Name
|
||||
$environmentName = $environment.Value
|
||||
|
||||
CreateEnvironment $environmentName
|
||||
$appId = CreateWorkloadIdentity $environmentAbbr $environmentName
|
||||
CreateResourceGroup $environmentAbbr $appId
|
||||
SetEnvironmentVariablesAndSecrets $environmentAbbr $environmentName $appId
|
||||
}
|
||||
|
||||
Write-Host "✅ Done"
|
||||
10
Directory.Build.props
Normal file
10
Directory.Build.props
Normal file
@@ -0,0 +1,10 @@
|
||||
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
38
Directory.Packages.props
Normal file
38
Directory.Packages.props
Normal file
@@ -0,0 +1,38 @@
|
||||
<!-- For more info on central package management go to https://devblogs.microsoft.com/nuget/introducing-central-package-management/ -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Ardalis.GuardClauses" Version="4.2.0" />
|
||||
<PackageVersion Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.10.4" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.8.1" />
|
||||
<PackageVersion Include="MediatR" Version="12.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.69" />
|
||||
<PackageVersion Include="NSwag.AspNetCore" Version="14.0.3" />
|
||||
<PackageVersion Include="NSwag.MSBuild" Version="14.0.3" />
|
||||
<PackageVersion Include="nunit" Version="3.14.0" />
|
||||
<PackageVersion Include="NUnit.Analyzers" Version="3.9.0" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageVersion Include="Respawn" Version="6.1.0" />
|
||||
<PackageVersion Include="Testcontainers.MsSql" Version="3.6.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
91
Hutopy.sln
Normal file
91
Hutopy.sln
Normal file
@@ -0,0 +1,91 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "src\Domain\Domain.csproj", "{C7E89A3E-A631-4760-8D61-BD1EAB1C4E69}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Application\Application.csproj", "{34C0FACD-F3D9-400C-8945-554DD6B0819A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{117DA02F-5274-4565-ACC6-DA9B6E568B09}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6ED356A7-8B47-4613-AD01-C85CF28491BD}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{664D406C-2F83-48F0-BFC3-408D5CB53C65}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application.UnitTests", "tests\Application.UnitTests\Application.UnitTests.csproj", "{DEFF4009-1FAB-4392-80B6-707E2DC5C00B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.UnitTests", "tests\Domain.UnitTests\Domain.UnitTests.csproj", "{DC37FD87-552C-4613-9F16-1537CA522898}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E2DA20AA-28D1-455C-BF50-C49A8F831633}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
Directory.Build.props = Directory.Build.props
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
global.json = global.json
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{4E4EE20C-F06A-4A1B-851F-C5577796941C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application.FunctionalTests", "tests\Application.FunctionalTests\Application.FunctionalTests.csproj", "{EA6127A5-94C9-4C31-AD11-E6811B92B520}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure.IntegrationTests", "tests\Infrastructure.IntegrationTests\Infrastructure.IntegrationTests.csproj", "{01FA6786-921D-4CE8-8C50-4FDA66C9477D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C7E89A3E-A631-4760-8D61-BD1EAB1C4E69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C7E89A3E-A631-4760-8D61-BD1EAB1C4E69}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C7E89A3E-A631-4760-8D61-BD1EAB1C4E69}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C7E89A3E-A631-4760-8D61-BD1EAB1C4E69}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{34C0FACD-F3D9-400C-8945-554DD6B0819A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{34C0FACD-F3D9-400C-8945-554DD6B0819A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{34C0FACD-F3D9-400C-8945-554DD6B0819A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{34C0FACD-F3D9-400C-8945-554DD6B0819A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{117DA02F-5274-4565-ACC6-DA9B6E568B09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{117DA02F-5274-4565-ACC6-DA9B6E568B09}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{117DA02F-5274-4565-ACC6-DA9B6E568B09}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{117DA02F-5274-4565-ACC6-DA9B6E568B09}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DEFF4009-1FAB-4392-80B6-707E2DC5C00B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DEFF4009-1FAB-4392-80B6-707E2DC5C00B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DEFF4009-1FAB-4392-80B6-707E2DC5C00B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DEFF4009-1FAB-4392-80B6-707E2DC5C00B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC37FD87-552C-4613-9F16-1537CA522898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DC37FD87-552C-4613-9F16-1537CA522898}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DC37FD87-552C-4613-9F16-1537CA522898}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC37FD87-552C-4613-9F16-1537CA522898}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EA6127A5-94C9-4C31-AD11-E6811B92B520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EA6127A5-94C9-4C31-AD11-E6811B92B520}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EA6127A5-94C9-4C31-AD11-E6811B92B520}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EA6127A5-94C9-4C31-AD11-E6811B92B520}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{01FA6786-921D-4CE8-8C50-4FDA66C9477D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01FA6786-921D-4CE8-8C50-4FDA66C9477D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01FA6786-921D-4CE8-8C50-4FDA66C9477D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{01FA6786-921D-4CE8-8C50-4FDA66C9477D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{C7E89A3E-A631-4760-8D61-BD1EAB1C4E69} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
|
||||
{34C0FACD-F3D9-400C-8945-554DD6B0819A} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
|
||||
{117DA02F-5274-4565-ACC6-DA9B6E568B09} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
|
||||
{DEFF4009-1FAB-4392-80B6-707E2DC5C00B} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
|
||||
{DC37FD87-552C-4613-9F16-1537CA522898} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
|
||||
{4E4EE20C-F06A-4A1B-851F-C5577796941C} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
|
||||
{EA6127A5-94C9-4C31-AD11-E6811B92B520} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
|
||||
{01FA6786-921D-4CE8-8C50-4FDA66C9477D} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3CB609D9-5D54-4C11-A371-DAAC8B74E430}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
75
README.md
Normal file
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Hutopy
|
||||
|
||||
## Pattern used
|
||||
- Clean Architecture
|
||||
- Guards ( Fail fast ) : https://github.com/ardalis/GuardClauses
|
||||
|
||||
## Tools
|
||||
- Install Docker : https://www.docker.com/get-started/
|
||||
- Install sql server management ( or preffered tool ) : https://learn.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver16#download-ssms
|
||||
|
||||
## Database setup in docker for local dev
|
||||
```
|
||||
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=<YourPassword>" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest
|
||||
```
|
||||
|
||||
Or with a mounted volume to persist data on the computer instead ( persist data even if the container is deleted )
|
||||
```
|
||||
docker run -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=<YourPassword>' -p 1433:1433 -v C:\dev\DockerVolumes\SqlServer-Utopy-1\data:/var/opt/mssql/data -v C:\dev\DockerVolumes\SqlServer-Utopy-1\log:/var/opt/mssql/log -v C:\dev\DockerVolumes\SqlServer-Utopy-1\secrets:/var/opt/mssql/secrets -d mcr.microsoft.com/mssql/server:2022-latest
|
||||
```
|
||||
|
||||
Set your password in an env var for the connection string. Windows : $Env:DB_PASSWORD = "YourPassword"
|
||||
|
||||
## Build
|
||||
|
||||
Run `dotnet build -tl` to build the solution.
|
||||
|
||||
## Run
|
||||
|
||||
To run the web application:
|
||||
|
||||
```bash
|
||||
cd .\src\Web\
|
||||
dotnet watch run
|
||||
```
|
||||
|
||||
Navigate to https://localhost:5001. The application will automatically reload if you change any of the source files.
|
||||
|
||||
## Code Styles & Formatting
|
||||
|
||||
The template includes [EditorConfig](https://editorconfig.org/) support to help maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The **.editorconfig** file defines the coding styles applicable to this solution.
|
||||
|
||||
## Code Scaffolding
|
||||
|
||||
Scaffold new commands and queries.
|
||||
|
||||
Start in the `.\src\Application\` folder.
|
||||
|
||||
Create a new command:
|
||||
|
||||
```
|
||||
dotnet new ca-usecase --name CreateTodoList --feature-name TodoLists --usecase-type command --return-type int
|
||||
```
|
||||
|
||||
Create a new query:
|
||||
|
||||
```
|
||||
dotnet new ca-usecase -n GetTodos -fn TodoLists -ut query -rt TodosVm
|
||||
```
|
||||
|
||||
If you encounter the error *"No templates or subcommands found matching: 'ca-usecase'."*, install the template and try again:
|
||||
|
||||
```bash
|
||||
dotnet new install Clean.Architecture.Solution.Template::8.0.4
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
The solution contains unit, integration, and functional tests.
|
||||
|
||||
- Using Moq, Nunit, Respawn, FluentAssertions
|
||||
|
||||
To run the tests:
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
6
global.json
Normal file
6
global.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
19
src/Application/Application.csproj
Normal file
19
src/Application/Application.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Application</RootNamespace>
|
||||
<AssemblyName>Hutopy.Application</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ardalis.GuardClauses" />
|
||||
<PackageReference Include="AutoMapper" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
79
src/Application/Common/Behaviours/AuthorizationBehaviour.cs
Normal file
79
src/Application/Common/Behaviours/AuthorizationBehaviour.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Reflection;
|
||||
using Hutopy.Application.Common.Exceptions;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Security;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
|
||||
{
|
||||
private readonly IUser _user;
|
||||
private readonly IIdentityService _identityService;
|
||||
|
||||
public AuthorizationBehaviour(
|
||||
IUser user,
|
||||
IIdentityService identityService)
|
||||
{
|
||||
_user = user;
|
||||
_identityService = identityService;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
var authorizeAttributes = request.GetType().GetCustomAttributes<AuthorizeAttribute>();
|
||||
|
||||
if (authorizeAttributes.Any())
|
||||
{
|
||||
// Must be authenticated user
|
||||
if (_user.Id == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
// Role-based authorization
|
||||
var authorizeAttributesWithRoles = authorizeAttributes.Where(a => !string.IsNullOrWhiteSpace(a.Roles));
|
||||
|
||||
if (authorizeAttributesWithRoles.Any())
|
||||
{
|
||||
var authorized = false;
|
||||
|
||||
foreach (var roles in authorizeAttributesWithRoles.Select(a => a.Roles.Split(',')))
|
||||
{
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var isInRole = await _identityService.IsInRoleAsync(_user.Id, role.Trim());
|
||||
if (isInRole)
|
||||
{
|
||||
authorized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Must be a member of at least one role in roles
|
||||
if (!authorized)
|
||||
{
|
||||
throw new ForbiddenAccessException();
|
||||
}
|
||||
}
|
||||
|
||||
// Policy-based authorization
|
||||
var authorizeAttributesWithPolicies = authorizeAttributes.Where(a => !string.IsNullOrWhiteSpace(a.Policy));
|
||||
if (authorizeAttributesWithPolicies.Any())
|
||||
{
|
||||
foreach (var policy in authorizeAttributesWithPolicies.Select(a => a.Policy))
|
||||
{
|
||||
var authorized = await _identityService.AuthorizeAsync(_user.Id, policy);
|
||||
|
||||
if (!authorized)
|
||||
{
|
||||
throw new ForbiddenAccessException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User is authorized / authorization not required
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
34
src/Application/Common/Behaviours/LoggingBehaviour.cs
Normal file
34
src/Application/Common/Behaviours/LoggingBehaviour.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using MediatR.Pipeline;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where TRequest : notnull
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IUser _user;
|
||||
private readonly IIdentityService _identityService;
|
||||
|
||||
public LoggingBehaviour(ILogger<TRequest> logger, IUser user, IIdentityService identityService)
|
||||
{
|
||||
_logger = logger;
|
||||
_user = user;
|
||||
_identityService = identityService;
|
||||
}
|
||||
|
||||
public async Task Process(TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _user.Id ?? string.Empty;
|
||||
string? userName = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
userName = await _identityService.GetUserNameAsync(userId);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Hutopy Request: {Name} {@UserId} {@UserName} {@Request}",
|
||||
requestName, userId, userName, request);
|
||||
}
|
||||
}
|
||||
53
src/Application/Common/Behaviours/PerformanceBehaviour.cs
Normal file
53
src/Application/Common/Behaviours/PerformanceBehaviour.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Diagnostics;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
|
||||
{
|
||||
private readonly Stopwatch _timer;
|
||||
private readonly ILogger<TRequest> _logger;
|
||||
private readonly IUser _user;
|
||||
private readonly IIdentityService _identityService;
|
||||
|
||||
public PerformanceBehaviour(
|
||||
ILogger<TRequest> logger,
|
||||
IUser user,
|
||||
IIdentityService identityService)
|
||||
{
|
||||
_timer = new Stopwatch();
|
||||
|
||||
_logger = logger;
|
||||
_user = user;
|
||||
_identityService = identityService;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
_timer.Start();
|
||||
|
||||
var response = await next();
|
||||
|
||||
_timer.Stop();
|
||||
|
||||
var elapsedMilliseconds = _timer.ElapsedMilliseconds;
|
||||
|
||||
if (elapsedMilliseconds > 500)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _user.Id ?? string.Empty;
|
||||
var userName = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
userName = await _identityService.GetUserNameAsync(userId);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Hutopy Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName} {@Request}",
|
||||
requestName, elapsedMilliseconds, userId, userName, request);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
|
||||
{
|
||||
private readonly ILogger<TRequest> _logger;
|
||||
|
||||
public UnhandledExceptionBehaviour(ILogger<TRequest> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await next();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
|
||||
_logger.LogError(ex, "Hutopy Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Application/Common/Behaviours/ValidationBehaviour.cs
Normal file
35
src/Application/Common/Behaviours/ValidationBehaviour.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using ValidationException = Hutopy.Application.Common.Exceptions.ValidationException;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||
|
||||
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
|
||||
{
|
||||
_validators = validators;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_validators.Any())
|
||||
{
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
|
||||
var validationResults = await Task.WhenAll(
|
||||
_validators.Select(v =>
|
||||
v.ValidateAsync(context, cancellationToken)));
|
||||
|
||||
var failures = validationResults
|
||||
.Where(r => r.Errors.Any())
|
||||
.SelectMany(r => r.Errors)
|
||||
.ToList();
|
||||
|
||||
if (failures.Any())
|
||||
throw new ValidationException(failures);
|
||||
}
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Application.Common.Exceptions;
|
||||
|
||||
public class ForbiddenAccessException : Exception
|
||||
{
|
||||
public ForbiddenAccessException() : base() { }
|
||||
}
|
||||
22
src/Application/Common/Exceptions/ValidationException.cs
Normal file
22
src/Application/Common/Exceptions/ValidationException.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace Hutopy.Application.Common.Exceptions;
|
||||
|
||||
public class ValidationException : Exception
|
||||
{
|
||||
public ValidationException()
|
||||
: base("One or more validation failures have occurred.")
|
||||
{
|
||||
Errors = new Dictionary<string, string[]>();
|
||||
}
|
||||
|
||||
public ValidationException(IEnumerable<ValidationFailure> failures)
|
||||
: this()
|
||||
{
|
||||
Errors = failures
|
||||
.GroupBy(e => e.PropertyName, e => e.ErrorMessage)
|
||||
.ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
|
||||
}
|
||||
|
||||
public IDictionary<string, string[]> Errors { get; }
|
||||
}
|
||||
12
src/Application/Common/Interfaces/IApplicationDbContext.cs
Normal file
12
src/Application/Common/Interfaces/IApplicationDbContext.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IApplicationDbContext
|
||||
{
|
||||
DbSet<TodoList> TodoLists { get; }
|
||||
|
||||
DbSet<TodoItem> TodoItems { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
16
src/Application/Common/Interfaces/IIdentityService.cs
Normal file
16
src/Application/Common/Interfaces/IIdentityService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IIdentityService
|
||||
{
|
||||
Task<string?> GetUserNameAsync(string userId);
|
||||
|
||||
Task<bool> IsInRoleAsync(string userId, string role);
|
||||
|
||||
Task<bool> AuthorizeAsync(string userId, string policyName);
|
||||
|
||||
Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password);
|
||||
|
||||
Task<Result> DeleteUserAsync(string userId);
|
||||
}
|
||||
6
src/Application/Common/Interfaces/IUser.cs
Normal file
6
src/Application/Common/Interfaces/IUser.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IUser
|
||||
{
|
||||
string? Id { get; }
|
||||
}
|
||||
12
src/Application/Common/Mappings/MappingExtensions.cs
Normal file
12
src/Application/Common/Mappings/MappingExtensions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.Common.Mappings;
|
||||
|
||||
public static class MappingExtensions
|
||||
{
|
||||
public static Task<PaginatedList<TDestination>> PaginatedListAsync<TDestination>(this IQueryable<TDestination> queryable, int pageNumber, int pageSize) where TDestination : class
|
||||
=> PaginatedList<TDestination>.CreateAsync(queryable.AsNoTracking(), pageNumber, pageSize);
|
||||
|
||||
public static Task<List<TDestination>> ProjectToListAsync<TDestination>(this IQueryable queryable, IConfigurationProvider configuration) where TDestination : class
|
||||
=> queryable.ProjectTo<TDestination>(configuration).AsNoTracking().ToListAsync();
|
||||
}
|
||||
19
src/Application/Common/Models/LookupDto.cs
Normal file
19
src/Application/Common/Models/LookupDto.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class LookupDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoList, LookupDto>();
|
||||
CreateMap<TodoItem, LookupDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Application/Common/Models/PaginatedList.cs
Normal file
29
src/Application/Common/Models/PaginatedList.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class PaginatedList<T>
|
||||
{
|
||||
public IReadOnlyCollection<T> Items { get; }
|
||||
public int PageNumber { get; }
|
||||
public int TotalPages { get; }
|
||||
public int TotalCount { get; }
|
||||
|
||||
public PaginatedList(IReadOnlyCollection<T> items, int count, int pageNumber, int pageSize)
|
||||
{
|
||||
PageNumber = pageNumber;
|
||||
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
|
||||
TotalCount = count;
|
||||
Items = items;
|
||||
}
|
||||
|
||||
public bool HasPreviousPage => PageNumber > 1;
|
||||
|
||||
public bool HasNextPage => PageNumber < TotalPages;
|
||||
|
||||
public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
|
||||
{
|
||||
var count = await source.CountAsync();
|
||||
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
|
||||
return new PaginatedList<T>(items, count, pageNumber, pageSize);
|
||||
}
|
||||
}
|
||||
24
src/Application/Common/Models/Result.cs
Normal file
24
src/Application/Common/Models/Result.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class Result
|
||||
{
|
||||
internal Result(bool succeeded, IEnumerable<string> errors)
|
||||
{
|
||||
Succeeded = succeeded;
|
||||
Errors = errors.ToArray();
|
||||
}
|
||||
|
||||
public bool Succeeded { get; init; }
|
||||
|
||||
public string[] Errors { get; init; }
|
||||
|
||||
public static Result Success()
|
||||
{
|
||||
return new Result(true, Array.Empty<string>());
|
||||
}
|
||||
|
||||
public static Result Failure(IEnumerable<string> errors)
|
||||
{
|
||||
return new Result(false, errors);
|
||||
}
|
||||
}
|
||||
23
src/Application/Common/Security/AuthorizeAttribute.cs
Normal file
23
src/Application/Common/Security/AuthorizeAttribute.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Hutopy.Application.Common.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the class this attribute is applied to requires authorization.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class AuthorizeAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
public AuthorizeAttribute() { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma delimited list of roles that are allowed to access the resource.
|
||||
/// </summary>
|
||||
public string Roles { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the policy name that determines access to the resource.
|
||||
/// </summary>
|
||||
public string Policy { get; set; } = string.Empty;
|
||||
}
|
||||
25
src/Application/DependencyInjection.cs
Normal file
25
src/Application/DependencyInjection.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Reflection;
|
||||
using Hutopy.Application.Common.Behaviours;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddAutoMapper(Assembly.GetExecutingAssembly());
|
||||
|
||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
6
src/Application/GlobalUsings.cs
Normal file
6
src/Application/GlobalUsings.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
global using AutoMapper;
|
||||
global using AutoMapper.QueryableExtensions;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using FluentValidation;
|
||||
global using MediatR;
|
||||
@@ -0,0 +1,40 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Entities;
|
||||
using Hutopy.Domain.Events;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.CreateTodoItem;
|
||||
|
||||
public record CreateTodoItemCommand : IRequest<int>
|
||||
{
|
||||
public int ListId { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
|
||||
public class CreateTodoItemCommandHandler : IRequestHandler<CreateTodoItemCommand, int>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateTodoItemCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<int> Handle(CreateTodoItemCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new TodoItem
|
||||
{
|
||||
ListId = request.ListId,
|
||||
Title = request.Title,
|
||||
Done = false
|
||||
};
|
||||
|
||||
entity.AddDomainEvent(new TodoItemCreatedEvent(entity));
|
||||
|
||||
_context.TodoItems.Add(entity);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Application.TodoItems.Commands.CreateTodoItem;
|
||||
|
||||
public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>
|
||||
{
|
||||
public CreateTodoItemCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.Title)
|
||||
.MaximumLength(200)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Events;
|
||||
using Hutopy.Application.Common.Security;
|
||||
using Hutopy.Domain.Constants;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.DeleteTodoItem;
|
||||
|
||||
[Authorize(Roles = Roles.Administrator)]
|
||||
[Authorize(Policy = Policies.CanDelete)]
|
||||
public record DeleteTodoItemCommand(int Id) : IRequest;
|
||||
|
||||
public class DeleteTodoItemCommandHandler : IRequestHandler<DeleteTodoItemCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteTodoItemCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Handle(DeleteTodoItemCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.TodoItems
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
_context.TodoItems.Remove(entity);
|
||||
|
||||
entity.AddDomainEvent(new TodoItemDeletedEvent(entity));
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
|
||||
|
||||
public record UpdateTodoItemCommand : IRequest
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public bool Done { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateTodoItemCommandHandler : IRequestHandler<UpdateTodoItemCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateTodoItemCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Handle(UpdateTodoItemCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.TodoItems
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
entity.Title = request.Title;
|
||||
entity.Done = request.Done;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
|
||||
|
||||
public class UpdateTodoItemCommandValidator : AbstractValidator<UpdateTodoItemCommand>
|
||||
{
|
||||
public UpdateTodoItemCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.Title)
|
||||
.MaximumLength(200)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Enums;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.UpdateTodoItemDetail;
|
||||
|
||||
public record UpdateTodoItemDetailCommand : IRequest
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public int ListId { get; init; }
|
||||
|
||||
public PriorityLevel Priority { get; init; }
|
||||
|
||||
public string? Note { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateTodoItemDetailCommandHandler : IRequestHandler<UpdateTodoItemDetailCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateTodoItemDetailCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Handle(UpdateTodoItemDetailCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.TodoItems
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
entity.ListId = request.ListId;
|
||||
entity.Priority = request.Priority;
|
||||
entity.Note = request.Note;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Hutopy.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.EventHandlers;
|
||||
|
||||
public class TodoItemCompletedEventHandler : INotificationHandler<TodoItemCompletedEvent>
|
||||
{
|
||||
private readonly ILogger<TodoItemCompletedEventHandler> _logger;
|
||||
|
||||
public TodoItemCompletedEventHandler(ILogger<TodoItemCompletedEventHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Handle(TodoItemCompletedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Hutopy Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Hutopy.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.EventHandlers;
|
||||
|
||||
public class TodoItemCreatedEventHandler : INotificationHandler<TodoItemCreatedEvent>
|
||||
{
|
||||
private readonly ILogger<TodoItemCreatedEventHandler> _logger;
|
||||
|
||||
public TodoItemCreatedEventHandler(ILogger<TodoItemCreatedEventHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Handle(TodoItemCreatedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Hutopy Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Mappings;
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
public record GetTodoItemsWithPaginationQuery : IRequest<PaginatedList<TodoItemBriefDto>>
|
||||
{
|
||||
public int ListId { get; init; }
|
||||
public int PageNumber { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 10;
|
||||
}
|
||||
|
||||
public class GetTodoItemsWithPaginationQueryHandler : IRequestHandler<GetTodoItemsWithPaginationQuery, PaginatedList<TodoItemBriefDto>>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetTodoItemsWithPaginationQueryHandler(IApplicationDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<PaginatedList<TodoItemBriefDto>> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
Console.WriteLine(request);
|
||||
return await _context.TodoItems
|
||||
.Where(x => x.ListId == request.ListId)
|
||||
.OrderBy(x => x.Title)
|
||||
.ProjectTo<TodoItemBriefDto>(_mapper.ConfigurationProvider)
|
||||
.PaginatedListAsync(request.PageNumber, request.PageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
public class GetTodoItemsWithPaginationQueryValidator : AbstractValidator<GetTodoItemsWithPaginationQuery>
|
||||
{
|
||||
public GetTodoItemsWithPaginationQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.ListId)
|
||||
.NotEmpty().WithMessage("ListId is required.");
|
||||
|
||||
RuleFor(x => x.PageNumber)
|
||||
.GreaterThanOrEqualTo(1).WithMessage("PageNumber at least greater than or equal to 1.");
|
||||
|
||||
RuleFor(x => x.PageSize)
|
||||
.GreaterThanOrEqualTo(1).WithMessage("PageSize at least greater than or equal to 1.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
public class TodoItemBriefDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public int ListId { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public bool Done { get; init; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoItem, TodoItemBriefDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.CreateTodoList;
|
||||
|
||||
public record CreateTodoListCommand : IRequest<int>
|
||||
{
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
|
||||
public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, int>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateTodoListCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<int> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new TodoList();
|
||||
|
||||
entity.Title = request.Title;
|
||||
|
||||
_context.TodoLists.Add(entity);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.CreateTodoList;
|
||||
|
||||
public class CreateTodoListCommandValidator : AbstractValidator<CreateTodoListCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateTodoListCommandValidator(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
RuleFor(v => v.Title)
|
||||
.NotEmpty()
|
||||
.MaximumLength(200)
|
||||
.MustAsync(BeUniqueTitle)
|
||||
.WithMessage("'{PropertyName}' must be unique.")
|
||||
.WithErrorCode("Unique");
|
||||
}
|
||||
|
||||
public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _context.TodoLists
|
||||
.AllAsync(l => l.Title != title, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.DeleteTodoList;
|
||||
|
||||
public record DeleteTodoListCommand(int Id) : IRequest;
|
||||
|
||||
public class DeleteTodoListCommandHandler : IRequestHandler<DeleteTodoListCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteTodoListCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Handle(DeleteTodoListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.TodoLists
|
||||
.Where(l => l.Id == request.Id)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
_context.TodoLists.Remove(entity);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Security;
|
||||
using Hutopy.Domain.Constants;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.PurgeTodoLists;
|
||||
|
||||
[Authorize(Roles = Roles.Administrator)]
|
||||
[Authorize(Policy = Policies.CanPurge)]
|
||||
public record PurgeTodoListsCommand : IRequest;
|
||||
|
||||
public class PurgeTodoListsCommandHandler : IRequestHandler<PurgeTodoListsCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public PurgeTodoListsCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Handle(PurgeTodoListsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
_context.TodoLists.RemoveRange(_context.TodoLists);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.UpdateTodoList;
|
||||
|
||||
public record UpdateTodoListCommand : IRequest
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateTodoListCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.TodoLists
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
entity.Title = request.Title;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.UpdateTodoList;
|
||||
|
||||
public class UpdateTodoListCommandValidator : AbstractValidator<UpdateTodoListCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateTodoListCommandValidator(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
RuleFor(v => v.Title)
|
||||
.NotEmpty()
|
||||
.MaximumLength(200)
|
||||
.MustAsync(BeUniqueTitle)
|
||||
.WithMessage("'{PropertyName}' must be unique.")
|
||||
.WithErrorCode("Unique");
|
||||
}
|
||||
|
||||
public async Task<bool> BeUniqueTitle(UpdateTodoListCommand model, string title, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _context.TodoLists
|
||||
.Where(l => l.Id != model.Id)
|
||||
.AllAsync(l => l.Title != title, cancellationToken);
|
||||
}
|
||||
}
|
||||
38
src/Application/TodoLists/Queries/GetTodos/GetTodos.cs
Normal file
38
src/Application/TodoLists/Queries/GetTodos/GetTodos.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Hutopy.Application.Common.Security;
|
||||
using Hutopy.Domain.Enums;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
[Authorize]
|
||||
public record GetTodosQuery : IRequest<TodosVm>;
|
||||
|
||||
public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, TodosVm>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetTodosQueryHandler(IApplicationDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<TodosVm> Handle(GetTodosQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new TodosVm
|
||||
{
|
||||
PriorityLevels = Enum.GetValues(typeof(PriorityLevel))
|
||||
.Cast<PriorityLevel>()
|
||||
.Select(p => new LookupDto { Id = (int)p, Title = p.ToString() })
|
||||
.ToList(),
|
||||
|
||||
Lists = await _context.TodoLists
|
||||
.AsNoTracking()
|
||||
.ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)
|
||||
.OrderBy(t => t.Title)
|
||||
.ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
27
src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs
Normal file
27
src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
public class TodoItemDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public int ListId { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public bool Done { get; init; }
|
||||
|
||||
public int Priority { get; init; }
|
||||
|
||||
public string? Note { get; init; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoItem, TodoItemDto>().ForMember(d => d.Priority,
|
||||
opt => opt.MapFrom(s => (int)s.Priority));
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs
Normal file
27
src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
public class TodoListDto
|
||||
{
|
||||
public TodoListDto()
|
||||
{
|
||||
Items = Array.Empty<TodoItemDto>();
|
||||
}
|
||||
|
||||
public int Id { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public string? Colour { get; init; }
|
||||
|
||||
public IReadOnlyCollection<TodoItemDto> Items { get; init; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoList, TodoListDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Application/TodoLists/Queries/GetTodos/TodosVm.cs
Normal file
10
src/Application/TodoLists/Queries/GetTodos/TodosVm.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
public class TodosVm
|
||||
{
|
||||
public IReadOnlyCollection<LookupDto> PriorityLevels { get; init; } = Array.Empty<LookupDto>();
|
||||
|
||||
public IReadOnlyCollection<TodoListDto> Lists { get; init; } = Array.Empty<TodoListDto>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Hutopy.Application.WeatherForecasts.Queries.GetWeatherForecasts;
|
||||
|
||||
public record GetWeatherForecastsQuery : IRequest<IEnumerable<WeatherForecast>>;
|
||||
|
||||
public class GetWeatherForecastsQueryHandler : IRequestHandler<GetWeatherForecastsQuery, IEnumerable<WeatherForecast>>
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
public async Task<IEnumerable<WeatherForecast>> Handle(GetWeatherForecastsQuery request, CancellationToken cancellationToken)
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
{
|
||||
var rng = new Random();
|
||||
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateTime.Now.AddDays(index),
|
||||
TemperatureC = rng.Next(-20, 55),
|
||||
Summary = Summaries[rng.Next(Summaries.Length)]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Hutopy.Application.WeatherForecasts.Queries.GetWeatherForecasts;
|
||||
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateTime Date { get; init; }
|
||||
|
||||
public int TemperatureC { get; init; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; init; }
|
||||
}
|
||||
12
src/Domain/Common/BaseAuditableEntity.cs
Normal file
12
src/Domain/Common/BaseAuditableEntity.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
public abstract class BaseAuditableEntity : BaseEntity
|
||||
{
|
||||
public DateTimeOffset Created { get; set; }
|
||||
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
|
||||
public string? LastModifiedBy { get; set; }
|
||||
}
|
||||
30
src/Domain/Common/BaseEntity.cs
Normal file
30
src/Domain/Common/BaseEntity.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
public abstract class BaseEntity
|
||||
{
|
||||
// This can easily be modified to be BaseEntity<T> and public T Id to support different key types.
|
||||
// Using non-generic integer types for simplicity
|
||||
public int Id { get; set; }
|
||||
|
||||
private readonly List<BaseEvent> _domainEvents = new();
|
||||
|
||||
[NotMapped]
|
||||
public IReadOnlyCollection<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
public void AddDomainEvent(BaseEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public void RemoveDomainEvent(BaseEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Remove(domainEvent);
|
||||
}
|
||||
|
||||
public void ClearDomainEvents()
|
||||
{
|
||||
_domainEvents.Clear();
|
||||
}
|
||||
}
|
||||
7
src/Domain/Common/BaseEvent.cs
Normal file
7
src/Domain/Common/BaseEvent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
public abstract class BaseEvent : INotification
|
||||
{
|
||||
}
|
||||
45
src/Domain/Common/ValueObject.cs
Normal file
45
src/Domain/Common/ValueObject.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
// Learn more: https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects
|
||||
public abstract class ValueObject
|
||||
{
|
||||
protected static bool EqualOperator(ValueObject left, ValueObject right)
|
||||
{
|
||||
if (left is null ^ right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return left?.Equals(right!) != false;
|
||||
}
|
||||
|
||||
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
|
||||
{
|
||||
return !(EqualOperator(left, right));
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<object> GetEqualityComponents();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null || obj.GetType() != GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var other = (ValueObject)obj;
|
||||
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
|
||||
foreach (var component in GetEqualityComponents())
|
||||
{
|
||||
hash.Add(component);
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
7
src/Domain/Constants/Policies.cs
Normal file
7
src/Domain/Constants/Policies.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Hutopy.Domain.Constants;
|
||||
|
||||
public abstract class Policies
|
||||
{
|
||||
public const string CanPurge = nameof(CanPurge);
|
||||
public const string CanDelete = nameof(CanDelete);
|
||||
}
|
||||
6
src/Domain/Constants/Roles.cs
Normal file
6
src/Domain/Constants/Roles.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Domain.Constants;
|
||||
|
||||
public abstract class Roles
|
||||
{
|
||||
public const string Administrator = nameof(Administrator);
|
||||
}
|
||||
12
src/Domain/Domain.csproj
Normal file
12
src/Domain/Domain.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Domain</RootNamespace>
|
||||
<AssemblyName>Hutopy.Domain</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
src/Domain/Entities/TodoItem.cs
Normal file
31
src/Domain/Entities/TodoItem.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Hutopy.Domain.Entities;
|
||||
|
||||
public class TodoItem : BaseAuditableEntity
|
||||
{
|
||||
public int ListId { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? Note { get; set; }
|
||||
|
||||
public PriorityLevel Priority { get; set; }
|
||||
|
||||
public DateTime? Reminder { get; set; }
|
||||
|
||||
private bool _done;
|
||||
public bool Done
|
||||
{
|
||||
get => _done;
|
||||
set
|
||||
{
|
||||
if (value && !_done)
|
||||
{
|
||||
AddDomainEvent(new TodoItemCompletedEvent(this));
|
||||
}
|
||||
|
||||
_done = value;
|
||||
}
|
||||
}
|
||||
|
||||
public TodoList List { get; set; } = null!;
|
||||
}
|
||||
10
src/Domain/Entities/TodoList.cs
Normal file
10
src/Domain/Entities/TodoList.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Hutopy.Domain.Entities;
|
||||
|
||||
public class TodoList : BaseAuditableEntity
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
|
||||
public Colour Colour { get; set; } = Colour.White;
|
||||
|
||||
public IList<TodoItem> Items { get; private set; } = new List<TodoItem>();
|
||||
}
|
||||
9
src/Domain/Enums/PriorityLevel.cs
Normal file
9
src/Domain/Enums/PriorityLevel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Hutopy.Domain.Enums;
|
||||
|
||||
public enum PriorityLevel
|
||||
{
|
||||
None = 0,
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3
|
||||
}
|
||||
11
src/Domain/Events/TodoItemCompletedEvent.cs
Normal file
11
src/Domain/Events/TodoItemCompletedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Domain.Events;
|
||||
|
||||
public class TodoItemCompletedEvent : BaseEvent
|
||||
{
|
||||
public TodoItemCompletedEvent(TodoItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public TodoItem Item { get; }
|
||||
}
|
||||
11
src/Domain/Events/TodoItemCreatedEvent.cs
Normal file
11
src/Domain/Events/TodoItemCreatedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Domain.Events;
|
||||
|
||||
public class TodoItemCreatedEvent : BaseEvent
|
||||
{
|
||||
public TodoItemCreatedEvent(TodoItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public TodoItem Item { get; }
|
||||
}
|
||||
11
src/Domain/Events/TodoItemDeletedEvent.cs
Normal file
11
src/Domain/Events/TodoItemDeletedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Domain.Events;
|
||||
|
||||
public class TodoItemDeletedEvent : BaseEvent
|
||||
{
|
||||
public TodoItemDeletedEvent(TodoItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public TodoItem Item { get; }
|
||||
}
|
||||
9
src/Domain/Exceptions/UnsupportedColourException.cs
Normal file
9
src/Domain/Exceptions/UnsupportedColourException.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Hutopy.Domain.Exceptions;
|
||||
|
||||
public class UnsupportedColourException : Exception
|
||||
{
|
||||
public UnsupportedColourException(string code)
|
||||
: base($"Colour \"{code}\" is unsupported.")
|
||||
{
|
||||
}
|
||||
}
|
||||
6
src/Domain/GlobalUsings.cs
Normal file
6
src/Domain/GlobalUsings.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
global using Hutopy.Domain.Common;
|
||||
global using Hutopy.Domain.Entities;
|
||||
global using Hutopy.Domain.Enums;
|
||||
global using Hutopy.Domain.Events;
|
||||
global using Hutopy.Domain.Exceptions;
|
||||
global using Hutopy.Domain.ValueObjects;
|
||||
69
src/Domain/ValueObjects/Colour.cs
Normal file
69
src/Domain/ValueObjects/Colour.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace Hutopy.Domain.ValueObjects;
|
||||
|
||||
public class Colour(string code) : ValueObject
|
||||
{
|
||||
public static Colour From(string code)
|
||||
{
|
||||
var colour = new Colour(code);
|
||||
|
||||
if (!SupportedColours.Contains(colour))
|
||||
{
|
||||
throw new UnsupportedColourException(code);
|
||||
}
|
||||
|
||||
return colour;
|
||||
}
|
||||
|
||||
public static Colour White => new("#FFFFFF");
|
||||
|
||||
public static Colour Red => new("#FF5733");
|
||||
|
||||
public static Colour Orange => new("#FFC300");
|
||||
|
||||
public static Colour Yellow => new("#FFFF66");
|
||||
|
||||
public static Colour Green => new("#CCFF99");
|
||||
|
||||
public static Colour Blue => new("#6666FF");
|
||||
|
||||
public static Colour Purple => new("#9966CC");
|
||||
|
||||
public static Colour Grey => new("#999999");
|
||||
|
||||
public string Code { get; private set; } = string.IsNullOrWhiteSpace(code)?"#000000":code;
|
||||
|
||||
public static implicit operator string(Colour colour)
|
||||
{
|
||||
return colour.ToString();
|
||||
}
|
||||
|
||||
public static explicit operator Colour(string code)
|
||||
{
|
||||
return From(code);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Code;
|
||||
}
|
||||
|
||||
protected static IEnumerable<Colour> SupportedColours
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return White;
|
||||
yield return Red;
|
||||
yield return Orange;
|
||||
yield return Yellow;
|
||||
yield return Green;
|
||||
yield return Blue;
|
||||
yield return Purple;
|
||||
yield return Grey;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<object> GetEqualityComponents()
|
||||
{
|
||||
yield return Code;
|
||||
}
|
||||
}
|
||||
23
src/Infrastructure/Data/ApplicationDbContext.cs
Normal file
23
src/Infrastructure/Data/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Entities;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data;
|
||||
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IApplicationDbContext
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<TodoList> TodoLists => Set<TodoList>();
|
||||
|
||||
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
}
|
||||
}
|
||||
109
src/Infrastructure/Data/ApplicationDbContextInitialiser.cs
Normal file
109
src/Infrastructure/Data/ApplicationDbContextInitialiser.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Hutopy.Domain.Constants;
|
||||
using Hutopy.Domain.Entities;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data;
|
||||
|
||||
public static class InitialiserExtensions
|
||||
{
|
||||
public static async Task InitialiseDatabaseAsync(this WebApplication app)
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
|
||||
var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();
|
||||
|
||||
await initialiser.InitialiseAsync();
|
||||
|
||||
await initialiser.SeedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class ApplicationDbContextInitialiser
|
||||
{
|
||||
private readonly ILogger<ApplicationDbContextInitialiser> _logger;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly RoleManager<IdentityRole> _roleManager;
|
||||
|
||||
public ApplicationDbContextInitialiser(ILogger<ApplicationDbContextInitialiser> logger, ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_context = context;
|
||||
_userManager = userManager;
|
||||
_roleManager = roleManager;
|
||||
}
|
||||
|
||||
public async Task InitialiseAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _context.Database.MigrateAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while initialising the database.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SeedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await TrySeedAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while seeding the database.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TrySeedAsync()
|
||||
{
|
||||
// Default roles
|
||||
var administratorRole = new IdentityRole(Roles.Administrator);
|
||||
|
||||
if (_roleManager.Roles.All(r => r.Name != administratorRole.Name))
|
||||
{
|
||||
await _roleManager.CreateAsync(administratorRole);
|
||||
}
|
||||
|
||||
// Default users
|
||||
var administrator = new ApplicationUser { UserName = "administrator@localhost", Email = "administrator@localhost" };
|
||||
|
||||
if (_userManager.Users.All(u => u.UserName != administrator.UserName))
|
||||
{
|
||||
await _userManager.CreateAsync(administrator, "Administrator1!");
|
||||
if (!string.IsNullOrWhiteSpace(administratorRole.Name))
|
||||
{
|
||||
await _userManager.AddToRolesAsync(administrator, new [] { administratorRole.Name });
|
||||
}
|
||||
}
|
||||
|
||||
// Default data
|
||||
// Seed, if necessary
|
||||
if (!_context.TodoLists.Any())
|
||||
{
|
||||
_context.TodoLists.Add(new TodoList
|
||||
{
|
||||
Title = "Todo List",
|
||||
Items =
|
||||
{
|
||||
new TodoItem { Title = "Make a todo list 📃" },
|
||||
new TodoItem { Title = "Check off the first item ✅" },
|
||||
new TodoItem { Title = "Realise you've already done two things on the list! 🤯"},
|
||||
new TodoItem { Title = "Reward yourself with a nice, long nap 🏆" },
|
||||
}
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Configurations;
|
||||
|
||||
public class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TodoItem> builder)
|
||||
{
|
||||
builder.Property(t => t.Title)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Configurations;
|
||||
|
||||
public class TodoListConfiguration : IEntityTypeConfiguration<TodoList>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TodoList> builder)
|
||||
{
|
||||
builder.Property(t => t.Title)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder
|
||||
.OwnsOne(b => b.Colour);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Interceptors;
|
||||
|
||||
public class AuditableEntityInterceptor : SaveChangesInterceptor
|
||||
{
|
||||
private readonly IUser _user;
|
||||
private readonly TimeProvider _dateTime;
|
||||
|
||||
public AuditableEntityInterceptor(
|
||||
IUser user,
|
||||
TimeProvider dateTime)
|
||||
{
|
||||
_user = user;
|
||||
_dateTime = dateTime;
|
||||
}
|
||||
|
||||
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
|
||||
{
|
||||
UpdateEntities(eventData.Context);
|
||||
|
||||
return base.SavingChanges(eventData, result);
|
||||
}
|
||||
|
||||
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
UpdateEntities(eventData.Context);
|
||||
|
||||
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||
}
|
||||
|
||||
public void UpdateEntities(DbContext? context)
|
||||
{
|
||||
if (context == null) return;
|
||||
|
||||
foreach (var entry in context.ChangeTracker.Entries<BaseAuditableEntity>())
|
||||
{
|
||||
if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())
|
||||
{
|
||||
var utcNow = _dateTime.GetUtcNow();
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
entry.Entity.CreatedBy = _user.Id;
|
||||
entry.Entity.Created = utcNow;
|
||||
}
|
||||
entry.Entity.LastModifiedBy = _user.Id;
|
||||
entry.Entity.LastModified = utcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static bool HasChangedOwnedEntities(this EntityEntry entry) =>
|
||||
entry.References.Any(r =>
|
||||
r.TargetEntry != null &&
|
||||
r.TargetEntry.Metadata.IsOwned() &&
|
||||
(r.TargetEntry.State == EntityState.Added || r.TargetEntry.State == EntityState.Modified));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Hutopy.Domain.Common;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Interceptors;
|
||||
|
||||
public class DispatchDomainEventsInterceptor : SaveChangesInterceptor
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public DispatchDomainEventsInterceptor(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
|
||||
{
|
||||
DispatchDomainEvents(eventData.Context).GetAwaiter().GetResult();
|
||||
|
||||
return base.SavingChanges(eventData, result);
|
||||
|
||||
}
|
||||
|
||||
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await DispatchDomainEvents(eventData.Context);
|
||||
|
||||
return await base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DispatchDomainEvents(DbContext? context)
|
||||
{
|
||||
if (context == null) return;
|
||||
|
||||
var entities = context.ChangeTracker
|
||||
.Entries<BaseEntity>()
|
||||
.Where(e => e.Entity.DomainEvents.Any())
|
||||
.Select(e => e.Entity);
|
||||
|
||||
var domainEvents = entities
|
||||
.SelectMany(e => e.DomainEvents)
|
||||
.ToList();
|
||||
|
||||
entities.ToList().ForEach(e => e.ClearDomainEvents());
|
||||
|
||||
foreach (var domainEvent in domainEvents)
|
||||
await _mediator.Publish(domainEvent);
|
||||
}
|
||||
}
|
||||
399
src/Infrastructure/Data/Migrations/00000000000000_InitialCreate.Designer.cs
generated
Normal file
399
src/Infrastructure/Data/Migrations/00000000000000_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,399 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("00000000000000_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.0-preview.6.23329.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("Done")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("ListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("Reminder")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId");
|
||||
|
||||
b.ToTable("TodoItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TodoLists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Domain.Entities.TodoList", "List")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.OwnsOne("Hutopy.Domain.ValueObjects.Colour", "Colour", b1 =>
|
||||
{
|
||||
b1.Property<int>("TodoListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b1.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.HasKey("TodoListId");
|
||||
|
||||
b1.ToTable("TodoLists");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TodoListId");
|
||||
});
|
||||
|
||||
b.Navigation("Colour")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TodoLists",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Colour_Code = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Created = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TodoLists", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TodoItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ListId = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Note = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Priority = table.Column<int>(type: "int", nullable: false),
|
||||
Reminder = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Done = table.Column<bool>(type: "bit", nullable: false),
|
||||
Created = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TodoItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TodoItems_TodoLists_ListId",
|
||||
column: x => x.ListId,
|
||||
principalTable: "TodoLists",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true,
|
||||
filter: "[NormalizedName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true,
|
||||
filter: "[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TodoItems_ListId",
|
||||
table: "TodoItems",
|
||||
column: "ListId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TodoItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TodoLists");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.0-preview.6.23329.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("Done")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("ListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("Reminder")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId");
|
||||
|
||||
b.ToTable("TodoItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TodoLists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Domain.Entities.TodoList", "List")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.OwnsOne("Hutopy.Domain.ValueObjects.Colour", "Colour", b1 =>
|
||||
{
|
||||
b1.Property<int>("TodoListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b1.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.HasKey("TodoListId");
|
||||
|
||||
b1.ToTable("TodoLists");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TodoListId");
|
||||
});
|
||||
|
||||
b.Navigation("Colour")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/Infrastructure/DependencyInjection.cs
Normal file
58
src/Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Constants;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Infrastructure.Data.Interceptors;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Replace password in the connection string with env var.
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection") ?? "";
|
||||
var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD");
|
||||
|
||||
connectionString = connectionString.Replace("{DB_PASSWORD}", dbPassword);
|
||||
|
||||
Guard.Against.Null(connectionString, message: "Connection string 'DefaultConnection' not found.");
|
||||
|
||||
services.AddScoped<ISaveChangesInterceptor, AuditableEntityInterceptor>();
|
||||
services.AddScoped<ISaveChangesInterceptor, DispatchDomainEventsInterceptor>();
|
||||
|
||||
services.AddDbContext<ApplicationDbContext>((sp, options) =>
|
||||
{
|
||||
options.AddInterceptors(sp.GetServices<ISaveChangesInterceptor>());
|
||||
|
||||
options.UseSqlServer(connectionString);
|
||||
});
|
||||
|
||||
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
|
||||
|
||||
services.AddScoped<ApplicationDbContextInitialiser>();
|
||||
|
||||
services.AddAuthentication()
|
||||
.AddBearerToken(IdentityConstants.BearerScheme);
|
||||
|
||||
services.AddAuthorizationBuilder();
|
||||
|
||||
services
|
||||
.AddIdentityCore<ApplicationUser>()
|
||||
.AddRoles<IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddApiEndpoints();
|
||||
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.AddTransient<IIdentityService, IdentityService>();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator)));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
1
src/Infrastructure/GlobalUsings.cs
Normal file
1
src/Infrastructure/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
7
src/Infrastructure/Identity/ApplicationUser.cs
Normal file
7
src/Infrastructure/Identity/ApplicationUser.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
}
|
||||
14
src/Infrastructure/Identity/IdentityResultExtensions.cs
Normal file
14
src/Infrastructure/Identity/IdentityResultExtensions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public static class IdentityResultExtensions
|
||||
{
|
||||
public static Result ToApplicationResult(this IdentityResult result)
|
||||
{
|
||||
return result.Succeeded
|
||||
? Result.Success()
|
||||
: Result.Failure(result.Errors.Select(e => e.Description));
|
||||
}
|
||||
}
|
||||
80
src/Infrastructure/Identity/IdentityService.cs
Normal file
80
src/Infrastructure/Identity/IdentityService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public class IdentityService : IIdentityService
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _userClaimsPrincipalFactory;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public IdentityService(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userClaimsPrincipalFactory = userClaimsPrincipalFactory;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
public async Task<string?> GetUserNameAsync(string userId)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
return user?.UserName;
|
||||
}
|
||||
|
||||
public async Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password)
|
||||
{
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = userName,
|
||||
Email = userName,
|
||||
};
|
||||
|
||||
var result = await _userManager.CreateAsync(user, password);
|
||||
|
||||
return (result.ToApplicationResult(), user.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> IsInRoleAsync(string userId, string role)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
return user != null && await _userManager.IsInRoleAsync(user, role);
|
||||
}
|
||||
|
||||
public async Task<bool> AuthorizeAsync(string userId, string policyName)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var principal = await _userClaimsPrincipalFactory.CreateAsync(user);
|
||||
|
||||
var result = await _authorizationService.AuthorizeAsync(principal, policyName);
|
||||
|
||||
return result.Succeeded;
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteUserAsync(string userId)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
return user != null ? await DeleteUserAsync(user) : Result.Success();
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteUserAsync(ApplicationUser user)
|
||||
{
|
||||
var result = await _userManager.DeleteAsync(user);
|
||||
|
||||
return result.ToApplicationResult();
|
||||
}
|
||||
}
|
||||
20
src/Infrastructure/Infrastructure.csproj
Normal file
20
src/Infrastructure/Infrastructure.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Infrastructure</RootNamespace>
|
||||
<AssemblyName>Hutopy.Infrastructure</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
66
src/Web/DependencyInjection.cs
Normal file
66
src/Web/DependencyInjection.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Azure.Identity;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using NSwag;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
services.AddScoped<IUser, CurrentUser>();
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddHealthChecks()
|
||||
.AddDbContextCheck<ApplicationDbContext>();
|
||||
|
||||
services.AddExceptionHandler<CustomExceptionHandler>();
|
||||
|
||||
services.AddRazorPages();
|
||||
|
||||
// Customise default API behaviour
|
||||
services.Configure<ApiBehaviorOptions>(options =>
|
||||
options.SuppressModelStateInvalidFilter = true);
|
||||
|
||||
services.AddEndpointsApiExplorer();
|
||||
|
||||
services.AddOpenApiDocument((configure, sp) =>
|
||||
{
|
||||
configure.Title = "Hutopy API";
|
||||
|
||||
// Add JWT
|
||||
configure.AddSecurity("JWT", Enumerable.Empty<string>(), new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.ApiKey,
|
||||
Name = "Authorization",
|
||||
In = OpenApiSecurityApiKeyLocation.Header,
|
||||
Description = "Type into the textbox: Bearer {your JWT token}."
|
||||
});
|
||||
|
||||
configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection services, ConfigurationManager configuration)
|
||||
{
|
||||
var keyVaultUri = configuration["KeyVaultUri"];
|
||||
if (!string.IsNullOrWhiteSpace(keyVaultUri))
|
||||
{
|
||||
configuration.AddAzureKeyVault(
|
||||
new Uri(keyVaultUri),
|
||||
new DefaultAzureCredential());
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
52
src/Web/Endpoints/TodoItems.cs
Normal file
52
src/Web/Endpoints/TodoItems.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.DeleteTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.UpdateTodoItemDetail;
|
||||
using Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class TodoItems : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetTodoItemsWithPagination)
|
||||
.MapPost(CreateTodoItem)
|
||||
.MapPut(UpdateTodoItem, "{id}")
|
||||
.MapPut(UpdateTodoItemDetail, "UpdateDetail/{id}")
|
||||
.MapDelete(DeleteTodoItem, "{id}");
|
||||
}
|
||||
|
||||
public Task<PaginatedList<TodoItemBriefDto>> GetTodoItemsWithPagination(ISender sender, [AsParameters] GetTodoItemsWithPaginationQuery query)
|
||||
{
|
||||
return sender.Send(query);
|
||||
}
|
||||
|
||||
public Task<int> CreateTodoItem(ISender sender, CreateTodoItemCommand command)
|
||||
{
|
||||
return sender.Send(command);
|
||||
}
|
||||
|
||||
public async Task<IResult> UpdateTodoItem(ISender sender, int id, UpdateTodoItemCommand command)
|
||||
{
|
||||
if (id != command.Id) return Results.BadRequest();
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
public async Task<IResult> UpdateTodoItemDetail(ISender sender, int id, UpdateTodoItemDetailCommand command)
|
||||
{
|
||||
if (id != command.Id) return Results.BadRequest();
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
public async Task<IResult> DeleteTodoItem(ISender sender, int id)
|
||||
{
|
||||
await sender.Send(new DeleteTodoItemCommand(id));
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
42
src/Web/Endpoints/TodoLists.cs
Normal file
42
src/Web/Endpoints/TodoLists.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
|
||||
using Hutopy.Application.TodoLists.Commands.DeleteTodoList;
|
||||
using Hutopy.Application.TodoLists.Commands.UpdateTodoList;
|
||||
using Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class TodoLists : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetTodoLists)
|
||||
.MapPost(CreateTodoList)
|
||||
.MapPut(UpdateTodoList, "{id}")
|
||||
.MapDelete(DeleteTodoList, "{id}");
|
||||
}
|
||||
|
||||
public Task<TodosVm> GetTodoLists(ISender sender)
|
||||
{
|
||||
return sender.Send(new GetTodosQuery());
|
||||
}
|
||||
|
||||
public Task<int> CreateTodoList(ISender sender, CreateTodoListCommand command)
|
||||
{
|
||||
return sender.Send(command);
|
||||
}
|
||||
|
||||
public async Task<IResult> UpdateTodoList(ISender sender, int id, UpdateTodoListCommand command)
|
||||
{
|
||||
if (id != command.Id) return Results.BadRequest();
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
public async Task<IResult> DeleteTodoList(ISender sender, int id)
|
||||
{
|
||||
await sender.Send(new DeleteTodoListCommand(id));
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
12
src/Web/Endpoints/Users.cs
Normal file
12
src/Web/Endpoints/Users.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class Users : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.MapIdentityApi<ApplicationUser>();
|
||||
}
|
||||
}
|
||||
18
src/Web/Endpoints/WeatherForecasts.cs
Normal file
18
src/Web/Endpoints/WeatherForecasts.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Hutopy.Application.WeatherForecasts.Queries.GetWeatherForecasts;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class WeatherForecasts : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetWeatherForecasts);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WeatherForecast>> GetWeatherForecasts(ISender sender)
|
||||
{
|
||||
return await sender.Send(new GetWeatherForecastsQuery());
|
||||
}
|
||||
}
|
||||
3
src/Web/GlobalUsings.cs
Normal file
3
src/Web/GlobalUsings.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
global using Hutopy.Web.Infrastructure;
|
||||
global using MediatR;
|
||||
87
src/Web/Infrastructure/CustomExceptionHandler.cs
Normal file
87
src/Web/Infrastructure/CustomExceptionHandler.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Hutopy.Application.Common.Exceptions;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public class CustomExceptionHandler : IExceptionHandler
|
||||
{
|
||||
private readonly Dictionary<Type, Func<HttpContext, Exception, Task>> _exceptionHandlers;
|
||||
|
||||
public CustomExceptionHandler()
|
||||
{
|
||||
// Register known exception types and handlers.
|
||||
_exceptionHandlers = new()
|
||||
{
|
||||
{ typeof(ValidationException), HandleValidationException },
|
||||
{ typeof(NotFoundException), HandleNotFoundException },
|
||||
{ typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
|
||||
{ typeof(ForbiddenAccessException), HandleForbiddenAccessException },
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
var exceptionType = exception.GetType();
|
||||
|
||||
if (_exceptionHandlers.ContainsKey(exceptionType))
|
||||
{
|
||||
await _exceptionHandlers[exceptionType].Invoke(httpContext, exception);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task HandleValidationException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
var exception = (ValidationException)ex;
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ValidationProblemDetails(exception.Errors)
|
||||
{
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleNotFoundException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
var exception = (NotFoundException)ex;
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails()
|
||||
{
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
Title = "The specified resource was not found.",
|
||||
Detail = exception.Message
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleUnauthorizedAccessException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Status = StatusCodes.Status401Unauthorized,
|
||||
Title = "Unauthorized",
|
||||
Type = "https://tools.ietf.org/html/rfc7235#section-3.1"
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleForbiddenAccessException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Status = StatusCodes.Status403Forbidden,
|
||||
Title = "Forbidden",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3"
|
||||
});
|
||||
}
|
||||
}
|
||||
6
src/Web/Infrastructure/EndpointGroupBase.cs
Normal file
6
src/Web/Infrastructure/EndpointGroupBase.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public abstract class EndpointGroupBase
|
||||
{
|
||||
public abstract void Map(WebApplication app);
|
||||
}
|
||||
46
src/Web/Infrastructure/IEndpointRouteBuilderExtensions.cs
Normal file
46
src/Web/Infrastructure/IEndpointRouteBuilderExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class IEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static IEndpointRouteBuilder MapGet(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern = "")
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapGet(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapPost(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern = "")
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapPost(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapPut(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern)
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapPut(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapDelete(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern)
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapDelete(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
18
src/Web/Infrastructure/MethodInfoExtensions.cs
Normal file
18
src/Web/Infrastructure/MethodInfoExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class MethodInfoExtensions
|
||||
{
|
||||
public static bool IsAnonymous(this MethodInfo method)
|
||||
{
|
||||
var invalidChars = new[] { '<', '>' };
|
||||
return method.Name.Any(invalidChars.Contains);
|
||||
}
|
||||
|
||||
public static void AnonymousMethod(this IGuardClause guardClause, Delegate input)
|
||||
{
|
||||
if (input.Method.IsAnonymous())
|
||||
throw new ArgumentException("The endpoint name must be specified when using anonymous handlers.");
|
||||
}
|
||||
}
|
||||
37
src/Web/Infrastructure/WebApplicationExtensions.cs
Normal file
37
src/Web/Infrastructure/WebApplicationExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static RouteGroupBuilder MapGroup(this WebApplication app, EndpointGroupBase group)
|
||||
{
|
||||
var groupName = group.GetType().Name;
|
||||
|
||||
return app
|
||||
.MapGroup($"/api/{groupName}")
|
||||
.WithGroupName(groupName)
|
||||
.WithTags(groupName)
|
||||
.WithOpenApi();
|
||||
}
|
||||
|
||||
public static WebApplication MapEndpoints(this WebApplication app)
|
||||
{
|
||||
var endpointGroupType = typeof(EndpointGroupBase);
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
var endpointGroupTypes = assembly.GetExportedTypes()
|
||||
.Where(t => t.IsSubclassOf(endpointGroupType));
|
||||
|
||||
foreach (var type in endpointGroupTypes)
|
||||
{
|
||||
if (Activator.CreateInstance(type) is EndpointGroupBase instance)
|
||||
{
|
||||
instance.Map(app);
|
||||
}
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
26
src/Web/Pages/Error.cshtml
Normal file
26
src/Web/Pages/Error.cshtml
Normal file
@@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user