diff --git a/.azure/applications/graphql/main.bicep b/.azure/applications/graphql/main.bicep index 2d63bebe9..f655664ac 100644 --- a/.azure/applications/graphql/main.bicep +++ b/.azure/applications/graphql/main.bicep @@ -61,6 +61,12 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' name: containerAppEnvironmentName } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${namePrefix}-graphql-identity' + location: location + tags: tags +} + var containerAppEnvVars = [ { name: 'ASPNETCORE_ENVIRONMENT' @@ -74,6 +80,10 @@ var containerAppEnvVars = [ name: 'AZURE_APPCONFIG_URI' value: appConfiguration.properties.endpoint } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } ] var port = 8080 @@ -157,6 +167,7 @@ module containerApp '../../modules/containerApp/main.bicep' = { probes: probes port: port scale: scale + userAssignedIdentityId: managedIdentity.id } } @@ -164,7 +175,7 @@ module keyVaultReaderAccessPolicy '../../modules/keyvault/addReaderRoles.bicep' name: 'keyVaultReaderAccessPolicy-${containerAppName}' params: { keyvaultName: environmentKeyVaultResource.name - principalIds: [containerApp.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } @@ -172,7 +183,7 @@ module appConfigReaderAccessPolicy '../../modules/appConfiguration/addReaderRole name: 'appConfigReaderAccessPolicy-${containerAppName}' params: { appConfigurationName: appConfigurationName - principalIds: [containerApp.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } diff --git a/.azure/applications/sync-resource-policy-information-job/main.bicep b/.azure/applications/sync-resource-policy-information-job/main.bicep index 3c1b5f479..740691b41 100644 --- a/.azure/applications/sync-resource-policy-information-job/main.bicep +++ b/.azure/applications/sync-resource-policy-information-job/main.bicep @@ -46,6 +46,12 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' name: containerAppEnvironmentName } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${namePrefix}-sync-rp-info-identity' + location: location + tags: tags +} + var containerAppEnvVars = [ { name: 'Infrastructure__DialogDbConnectionString' @@ -63,6 +69,10 @@ var containerAppEnvVars = [ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' value: appInsightConnectionString } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } ] // Base URL for accessing secrets in the Key Vault @@ -94,6 +104,7 @@ module migrationJob '../../modules/containerAppJob/main.bicep' = { tags: tags cronExpression: jobSchedule args: 'sync-resource-policy-information' + userAssignedIdentityId: managedIdentity.id } } diff --git a/.azure/applications/sync-subject-resource-mappings-job/main.bicep b/.azure/applications/sync-subject-resource-mappings-job/main.bicep index a2d6203f5..3279e4d7e 100644 --- a/.azure/applications/sync-subject-resource-mappings-job/main.bicep +++ b/.azure/applications/sync-subject-resource-mappings-job/main.bicep @@ -46,6 +46,12 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' name: containerAppEnvironmentName } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${namePrefix}-sync-sr-mappings-identity' + location: location + tags: tags +} + var containerAppEnvVars = [ { name: 'Infrastructure__DialogDbConnectionString' @@ -63,6 +69,10 @@ var containerAppEnvVars = [ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' value: appInsightConnectionString } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } ] // Base URL for accessing secrets in the Key Vault @@ -94,6 +104,7 @@ module migrationJob '../../modules/containerAppJob/main.bicep' = { tags: tags cronExpression: jobSchedule args: 'sync-subject-resource-mappings' + userAssignedIdentityId: managedIdentity.id } } @@ -101,9 +112,9 @@ module keyVaultReaderAccessPolicy '../../modules/keyvault/addReaderRoles.bicep' name: 'keyVaultReaderAccessPolicy-${name}' params: { keyvaultName: environmentKeyVaultName - principalIds: [migrationJob.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } -output identityPrincipalId string = migrationJob.outputs.identityPrincipalId +output identityPrincipalId string = managedIdentity.properties.principalId output name string = migrationJob.outputs.name diff --git a/.azure/applications/web-api-eu/main.bicep b/.azure/applications/web-api-eu/main.bicep index 9f032afbf..b4cbd6592 100644 --- a/.azure/applications/web-api-eu/main.bicep +++ b/.azure/applications/web-api-eu/main.bicep @@ -60,6 +60,12 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' name: containerAppEnvironmentName } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${namePrefix}-webapi-eu-identity' + location: location + tags: tags +} + var containerAppEnvVars = [ { name: 'ASPNETCORE_ENVIRONMENT' @@ -77,6 +83,10 @@ var containerAppEnvVars = [ name: 'ASPNETCORE_URLS' value: 'http://+:8080' } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } ] @description('The scaling configuration for the container app') @@ -159,6 +169,7 @@ module containerApp '../../modules/containerApp/main.bicep' = { probes: probes revisionSuffix: revisionSuffix scale: scale + userAssignedIdentityId: managedIdentity.id } } @@ -166,7 +177,7 @@ module keyVaultReaderAccessPolicy '../../modules/keyvault/addReaderRoles.bicep' name: 'keyVaultReaderAccessPolicy-${containerAppName}' params: { keyvaultName: environmentKeyVaultResource.name - principalIds: [containerApp.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } @@ -174,7 +185,7 @@ module appConfigReaderAccessPolicy '../../modules/appConfiguration/addReaderRole name: 'appConfigReaderAccessPolicy-${containerAppName}' params: { appConfigurationName: appConfigurationName - principalIds: [containerApp.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } diff --git a/.azure/applications/web-api-migration-job/main.bicep b/.azure/applications/web-api-migration-job/main.bicep index 5df04957b..ca5857455 100644 --- a/.azure/applications/web-api-migration-job/main.bicep +++ b/.azure/applications/web-api-migration-job/main.bicep @@ -34,11 +34,21 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' name: containerAppEnvironmentName } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${namePrefix}-migration-job-identity' + location: location + tags: tags +} + var containerAppEnvVars = [ { name: 'Infrastructure__DialogDbConnectionString' secretRef: 'dbconnectionstring' } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } ] // https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions-deployment#example-1 @@ -62,6 +72,7 @@ module migrationJob '../../modules/containerAppJob/main.bicep' = { environmentVariables: containerAppEnvVars secrets: secrets tags: tags + userAssignedIdentityId: managedIdentity.id } } @@ -69,9 +80,9 @@ module keyVaultReaderAccessPolicy '../../modules/keyvault/addReaderRoles.bicep' name: 'keyVaultReaderAccessPolicy-${name}' params: { keyvaultName: environmentKeyVaultName - principalIds: [migrationJob.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } -output identityPrincipalId string = migrationJob.outputs.identityPrincipalId +output identityPrincipalId string = managedIdentity.properties.principalId output name string = migrationJob.outputs.name diff --git a/.azure/applications/web-api-so/main.bicep b/.azure/applications/web-api-so/main.bicep index d9085b77d..eb1feccc0 100644 --- a/.azure/applications/web-api-so/main.bicep +++ b/.azure/applications/web-api-so/main.bicep @@ -88,6 +88,12 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' name: containerAppEnvironmentName } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${namePrefix}-webapi-so-identity' + location: location + tags: tags +} + var containerAppEnvVars = [ { name: 'ASPNETCORE_ENVIRONMENT' @@ -105,6 +111,10 @@ var containerAppEnvVars = [ name: 'ASPNETCORE_URLS' value: 'http://+:8080' } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } ] resource environmentKeyVaultResource 'Microsoft.KeyVault/vaults@2023-07-01' existing = { @@ -160,6 +170,7 @@ module containerApp '../../modules/containerApp/main.bicep' = { port: port revisionSuffix: revisionSuffix scale: scale + userAssignedIdentityId: managedIdentity.id } } @@ -167,7 +178,7 @@ module keyVaultReaderAccessPolicy '../../modules/keyvault/addReaderRoles.bicep' name: 'keyVaultReaderAccessPolicy-${containerAppName}' params: { keyvaultName: environmentKeyVaultResource.name - principalIds: [containerApp.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } @@ -175,7 +186,7 @@ module appConfigReaderAccessPolicy '../../modules/appConfiguration/addReaderRole name: 'appConfigReaderAccessPolicy-${containerAppName}' params: { appConfigurationName: appConfigurationName - principalIds: [containerApp.outputs.identityPrincipalId] + principalIds: [managedIdentity.properties.principalId] } } diff --git a/.azure/infrastructure/main.bicep b/.azure/infrastructure/main.bicep index 11de7911f..f304318e1 100644 --- a/.azure/infrastructure/main.bicep +++ b/.azure/infrastructure/main.bicep @@ -51,9 +51,6 @@ param appConfigurationSku AppConfigurationSku import { Sku as AppInsightsSku } from '../modules/applicationInsights/create.bicep' param appInsightsSku AppInsightsSku -import { Sku as SlackNotifierSku } from '../modules/functionApp/slackNotifier.bicep' -param slackNotifierSku SlackNotifierSku - import { Sku as PostgresSku } from '../modules/postgreSql/create.bicep' import { StorageConfiguration as PostgresStorageConfig } from '../modules/postgreSql/create.bicep' @@ -127,16 +124,6 @@ module appInsights '../modules/applicationInsights/create.bicep' = { } } -module monitorWorkspace '../modules/monitor-workspace/main.bicep' = { - scope: resourceGroup - name: 'monitorWorkspace' - params: { - namePrefix: namePrefix - location: location - tags: tags - } -} - module apimAvailabilityTest '../modules/applicationInsights/availabilityTest.bicep' = { scope: resourceGroup name: 'apimAvailabilityTest' @@ -272,19 +259,6 @@ module copyEnvironmentSecrets '../modules/keyvault/copySecrets.bicep' = { } } -module slackNotifier '../modules/functionApp/slackNotifier.bicep' = { - name: 'slackNotifier' - scope: resourceGroup - params: { - location: location - keyVaultName: environmentKeyVault.outputs.name - namePrefix: namePrefix - applicationInsightsName: appInsights.outputs.appInsightsName - sku: slackNotifierSku - tags: tags - } -} - module containerAppIdentity '../modules/managedIdentity/main.bicep' = { scope: resourceGroup name: 'containerAppIdentity' @@ -303,31 +277,12 @@ module containerAppEnv '../modules/containerAppEnv/main.bicep' = { location: location appInsightWorkspaceName: appInsights.outputs.appInsightsWorkspaceName appInsightsConnectionString: appInsights.outputs.connectionString - monitorMetricsIngestionEndpoint: monitorWorkspace.outputs.containerAppEnvironmentMetricsIngestionEndpoint userAssignedIdentityId: containerAppIdentity.outputs.managedIdentityId subnetId: vnet.outputs.containerAppEnvironmentSubnetId tags: tags } } -module monitorMetricsPublisherRoles '../modules/monitor-workspace/addMetricsPublisherRoles.bicep' = { - scope: resourceGroup - name: 'monitorMetricsPublisherRoles' - params: { - monitorWorkspaceName: monitorWorkspace.outputs.monitorWorkspaceName - principalIds: [containerAppIdentity.outputs.managedIdentityPrincipalId] - } -} - -module appInsightsReaderAccessPolicy '../modules/applicationInsights/addReaderRoles.bicep' = { - scope: resourceGroup - name: 'appInsightsReaderAccessPolicy' - params: { - appInsightsName: appInsights.outputs.appInsightsName - principalIds: [slackNotifier.outputs.functionAppPrincipalId] - } -} - module postgresConnectionStringAppConfig '../modules/appConfiguration/upsertKeyValue.bicep' = { scope: resourceGroup name: 'AppConfig_Add_DialogDbConnectionString' @@ -352,15 +307,6 @@ module redisConnectionStringAppConfig '../modules/appConfiguration/upsertKeyValu } } -module keyVaultReaderAccessPolicy '../modules/keyvault/addReaderRoles.bicep' = { - scope: resourceGroup - name: 'keyVaultReaderAccessPolicyFunctions' - params: { - keyvaultName: environmentKeyVault.outputs.name - principalIds: [slackNotifier.outputs.functionAppPrincipalId] - } -} - output resourceGroupName string = resourceGroup.name output containerAppEnvId string = containerAppEnv.outputs.containerAppEnvId output environmentKeyVaultName string = environmentKeyVault.outputs.name diff --git a/.azure/infrastructure/prod.bicepparam b/.azure/infrastructure/prod.bicepparam index 2b0a624e7..14699dd41 100644 --- a/.azure/infrastructure/prod.bicepparam +++ b/.azure/infrastructure/prod.bicepparam @@ -24,11 +24,6 @@ param appConfigurationSku = { param appInsightsSku = { name: 'PerGB2018' } -param slackNotifierSku = { - storageAccountName: 'Standard_LRS' - applicationServicePlanName: 'Y1' - applicationServicePlanTier: 'Dynamic' -} param postgresConfiguration = { sku: { name: 'Standard_D8ads_v5' diff --git a/.azure/infrastructure/staging.bicepparam b/.azure/infrastructure/staging.bicepparam index c434a5469..e3230883c 100644 --- a/.azure/infrastructure/staging.bicepparam +++ b/.azure/infrastructure/staging.bicepparam @@ -24,11 +24,6 @@ param appConfigurationSku = { param appInsightsSku = { name: 'PerGB2018' } -param slackNotifierSku = { - storageAccountName: 'Standard_LRS' - applicationServicePlanName: 'Y1' - applicationServicePlanTier: 'Dynamic' -} param postgresConfiguration = { sku: { name: 'Standard_D4ads_v5' diff --git a/.azure/infrastructure/test.bicepparam b/.azure/infrastructure/test.bicepparam index e4ca02594..9f2fe769f 100644 --- a/.azure/infrastructure/test.bicepparam +++ b/.azure/infrastructure/test.bicepparam @@ -24,11 +24,6 @@ param appConfigurationSku = { param appInsightsSku = { name: 'PerGB2018' } -param slackNotifierSku = { - storageAccountName: 'Standard_LRS' - applicationServicePlanName: 'Y1' - applicationServicePlanTier: 'Dynamic' -} param postgresConfiguration = { sku: { name: 'Standard_B2s' diff --git a/.azure/infrastructure/yt01.bicepparam b/.azure/infrastructure/yt01.bicepparam index d37a727da..667c78ced 100644 --- a/.azure/infrastructure/yt01.bicepparam +++ b/.azure/infrastructure/yt01.bicepparam @@ -24,18 +24,13 @@ param appConfigurationSku = { param appInsightsSku = { name: 'PerGB2018' } -param slackNotifierSku = { - storageAccountName: 'Standard_LRS' - applicationServicePlanName: 'Y1' - applicationServicePlanTier: 'Dynamic' -} param postgresConfiguration = { sku: { name: 'Standard_D8ads_v5' tier: 'GeneralPurpose' } storage: { - storageSizeGB: 256 + storageSizeGB: 2048 autoGrow: 'Enabled' type: 'Premium_LRS' } diff --git a/.azure/modules/containerApp/main.bicep b/.azure/modules/containerApp/main.bicep index 9259e047e..c2f3c6979 100644 --- a/.azure/modules/containerApp/main.bicep +++ b/.azure/modules/containerApp/main.bicep @@ -58,9 +58,9 @@ param scale Scale = { rules: [] } -// TODO: Refactor to make userAssignedIdentityId a required parameter once all container apps use user-assigned identities -@description('The ID of the user-assigned managed identity (optional)') -param userAssignedIdentityId string = '' +@description('The ID of the user-assigned managed identity') +@minLength(1) +param userAssignedIdentityId string // Container app revision name does not allow '.' character var cleanedRevisionSuffix = replace(revisionSuffix, '.', '-') @@ -81,19 +81,19 @@ var ingress = { ipSecurityRestrictions: ipSecurityRestrictions } -var identityConfig = empty(userAssignedIdentityId) ? { - type: 'SystemAssigned' -} : { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentityId}': {} - } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { + name: last(split(userAssignedIdentityId, '/')) } resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { name: name location: location - identity: identityConfig + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentityId}': {} + } + } properties: { configuration: { ingress: ingress @@ -116,10 +116,6 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { tags: tags } -resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(userAssignedIdentityId)) { - name: last(split(userAssignedIdentityId, '/')) -} - -output identityPrincipalId string = empty(userAssignedIdentityId) ? containerApp.identity.principalId : managedIdentity.properties.principalId +output identityPrincipalId string = managedIdentity.properties.principalId output name string = containerApp.name output revisionName string = containerApp.properties.latestRevisionName diff --git a/.azure/modules/containerAppEnv/main.bicep b/.azure/modules/containerAppEnv/main.bicep index a05e598cb..c1fd26b5c 100644 --- a/.azure/modules/containerAppEnv/main.bicep +++ b/.azure/modules/containerAppEnv/main.bicep @@ -16,9 +16,6 @@ param appInsightWorkspaceName string @description('The Application Insights connection string') param appInsightsConnectionString string -@description('The metrics ingestion endpoint of the Azure Monitor workspace') -param monitorMetricsIngestionEndpoint string - @description('The ID of the user-assigned managed identity') param userAssignedIdentityId string @@ -57,18 +54,6 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-02-02-preview' logsConfiguration: { destinations: ['appInsights'] } - metricsConfiguration: { - destinations: ['metrics-ingestion'] - } - destinationsConfiguration: { - otlpConfigurations: [ - { - endpoint: monitorMetricsIngestionEndpoint - name: 'metrics-ingestion' - insecure: false - } - ] - } } } tags: tags diff --git a/.azure/modules/containerAppJob/main.bicep b/.azure/modules/containerAppJob/main.bicep index 8a1628018..836bbd250 100644 --- a/.azure/modules/containerAppJob/main.bicep +++ b/.azure/modules/containerAppJob/main.bicep @@ -25,6 +25,10 @@ param cronExpression string = '' @description('The container args for the job (optional)') param args string = '' +@description('The ID of the user-assigned managed identity') +@minLength(1) +param userAssignedIdentityId string + var isScheduled = !empty(cronExpression) var scheduledJobProperties = { @@ -42,11 +46,18 @@ var manualJobProperties = { } } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { + name: last(split(userAssignedIdentityId, '/')) +} + resource job 'Microsoft.App/jobs@2024-03-01' = { name: name location: location identity: { - type: 'SystemAssigned' + type: 'UserAssigned' + userAssignedIdentities: { + '${userAssignedIdentityId}': {} + } } properties: { configuration: union( @@ -72,5 +83,5 @@ resource job 'Microsoft.App/jobs@2024-03-01' = { tags: tags } -output identityPrincipalId string = job.identity.principalId +output identityPrincipalId string = managedIdentity.properties.principalId output name string = job.name diff --git a/.azure/modules/functionApp/appSettings.bicep b/.azure/modules/functionApp/appSettings.bicep deleted file mode 100644 index eeca797f2..000000000 --- a/.azure/modules/functionApp/appSettings.bicep +++ /dev/null @@ -1,18 +0,0 @@ -@description('The name of the web application') -param webAppName string - -@description('The new app settings to be applied') -param appSettings object - -@description('The current app settings of the web application') -param currentAppSettings object - -resource webApp 'Microsoft.Web/sites@2023-12-01' existing = { - name: webAppName -} - -resource siteconfig 'Microsoft.Web/sites/config@2023-12-01' = { - parent: webApp - name: 'appsettings' - properties: union(currentAppSettings, appSettings) -} diff --git a/.azure/modules/functionApp/slackNotifier.bicep b/.azure/modules/functionApp/slackNotifier.bicep deleted file mode 100644 index fbd0629eb..000000000 --- a/.azure/modules/functionApp/slackNotifier.bicep +++ /dev/null @@ -1,221 +0,0 @@ -import { uniqueStringBySubscriptionAndResourceGroup, uniqueResourceName } from '../../functions/resourceName.bicep' - -@description('The location where the resources will be deployed') -param location string - -@description('The name of the Application Insights resource') -param applicationInsightsName string - -@description('The prefix used for naming resources to ensure unique names') -param namePrefix string - -@description('The name of the Key Vault') -param keyVaultName string - -@description('Tags to apply to resources') -param tags object - -@export() -type Sku = { - storageAccountName: - | 'Standard_LRS' - | 'Standard_GRS' - | 'Standard_RAGRS' - | 'Standard_ZRS' - | 'Premium_LRS' - | 'Premium_ZRS' - applicationServicePlanName: - | 'F1' - | 'D1' - | 'B1' - | 'B2' - | 'B3' - | 'S1' - | 'S2' - | 'S3' - | 'P1' - | 'P2' - | 'P3' - | 'P1V2' - | 'P2V2' - | 'P3V2' - | 'I1' - | 'I2' - | 'I3' - | 'Y1' - | 'Y2' - | 'Y3' - | 'Y1v2' - | 'Y2v2' - | 'Y3v2' - | 'Y1v2Isolated' - | 'Y2v2Isolated' - | 'Y3v2Isolated' - applicationServicePlanTier: 'Free' | 'Shared' | 'Basic' | 'Dynamic' | 'Standard' | 'Premium' | 'Isolated' -} -@description('The SKU of the Slack Notifier') -param sku Sku - -// Storage account names only supports lower case and numbers -// todo: add name of function as param and turn this into a reusable module -// We use uniqueStringBySubscriptionAndResourceGroup directly here to avoid having too short storage account name. -// This should be refactored to use one common storage account. Or one storage account for all app functions. -var storageAccountName = take( - replace('${'${namePrefix}-sn'}-${uniqueStringBySubscriptionAndResourceGroup()}', '-', ''), - 24 -) - -resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' = { - name: storageAccountName - location: location - sku: { - name: sku.storageAccountName - } - kind: 'Storage' - properties: { - supportsHttpsTrafficOnly: true - defaultToOAuthAuthentication: true - minimumTlsVersion: 'TLS1_2' - } - tags: tags -} - -resource applicationServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { - name: '${namePrefix}-slacknotifier-asp' - location: location - sku: { - name: sku.applicationServicePlanName - tier: sku.applicationServicePlanTier - } - properties: {} - tags: tags -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} - -var functionAppNameMaxLength = 40 -var functionAppName = uniqueResourceName('${namePrefix}-slacknotifier-fa', functionAppNameMaxLength) -resource functionApp 'Microsoft.Web/sites@2023-12-01' = { - name: functionAppName - location: location - kind: 'functionapp' - identity: { - type: 'SystemAssigned' - } - properties: { - serverFarmId: applicationServicePlan.id - publicNetworkAccess: 'Enabled' - siteConfig: { - // Setting/updating appSettings in separate module in order to not delete already deployed functions, see below - } - httpsOnly: true - } - tags: tags -} - -var appSettings = { - AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' - WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' - WEBSITE_CONTENTSHARE: toLower(functionAppName) - FUNCTIONS_EXTENSION_VERSION: '~4' - APPINSIGHTS_INSTRUMENTATIONKEY: applicationInsights.properties.InstrumentationKey - Slack__WebhookUrl: '@Microsoft.KeyVault(VaultName=${keyVaultName};SecretName=Slack--Webhook--Url)' - FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' -} - -module updateAppSettings 'appSettings.bicep' = { - name: '${functionAppName}-appsettings' - params: { - webAppName: functionAppName - currentAppSettings: list(resourceId('Microsoft.Web/sites/config', functionAppName, 'appsettings'), '2023-01-01').properties - appSettings: appSettings - } -} - -var defaultFunctionKey = listkeys('${functionApp.id}/host/default', '2023-01-01').functionKeys.default -var forwardAlertToSlackTriggerUrl = 'https://${functionApp.properties.defaultHostName}/api/forwardalerttoslack?code=${defaultFunctionKey}' -resource notifyDevTeam 'Microsoft.Insights/actionGroups@2023-01-01' = { - name: '${namePrefix}-notify-devteam-ag' - location: 'Global' - properties: { - enabled: true - groupShortName: 'DevNotify' - azureFunctionReceivers: [ - { - name: functionApp.properties.defaultHostName - functionName: 'ForwardAlertToSlack' - functionAppResourceId: functionApp.id - httpTriggerUrl: forwardAlertToSlackTriggerUrl - useCommonAlertSchema: true - } - ] - } - tags: tags -} - -var query = ''' - union - (exceptions - | where not(customDimensions.['Service Type'] == 'API Management') - | where type != "ZiggyCreatures.Caching.Fusion.SyntheticTimeoutException"), - (traces - | where severityLevel >= 3 or (severityLevel >= 2 and customDimensions.SourceContext startswith "Digdir")) - | where customDimensions.RequestPath !startswith "/health" - | summarize Count = count() - by - Environment = tostring(customDimensions.EnvironmentName), - Type = itemType, - problemId, - tostring(customDimensions.MessageTemplate), - severityLevel - | extend SeverityLevel = case( - severityLevel == 0, "Verbose", - severityLevel == 1, "Information", - severityLevel == 2, "Warning", - severityLevel == 3, "Error", - severityLevel == 4, "Critical", - tostring(severityLevel) - ) - | extend Details = coalesce(problemId, customDimensions_MessageTemplate) - | extend Details = replace_string(Details, "Digdir.Domain.Dialogporten.", "") - | project Environment, Type, SeverityLevel, Count, Details - ''' -resource exceptionOccuredAlertRule 'Microsoft.Insights/scheduledQueryRules@2023-03-15-preview' = { - name: '${namePrefix}-exception-occured-sqr' - location: location - properties: { - enabled: true - severity: 1 - evaluationFrequency: 'PT5M' - windowSize: 'PT5M' - scopes: [applicationInsights.id] - autoMitigate: false - targetResourceTypes: [ - 'microsoft.insights/components' - ] - criteria: { - allOf: [ - { - query: query - operator: 'GreaterThan' - threshold: 0 - timeAggregation: 'Count' - failingPeriods: { - numberOfEvaluationPeriods: 1 - minFailingPeriodsToAlert: 1 - } - } - ] - } - actions: { - actionGroups: [ - notifyDevTeam.id - ] - } - } - tags: tags -} - -output functionAppPrincipalId string = functionApp.identity.principalId diff --git a/.azure/modules/monitor-workspace/addMetricsPublisherRoles.bicep b/.azure/modules/monitor-workspace/addMetricsPublisherRoles.bicep deleted file mode 100644 index 26ea44553..000000000 --- a/.azure/modules/monitor-workspace/addMetricsPublisherRoles.bicep +++ /dev/null @@ -1,25 +0,0 @@ -@description('The name of the Monitor workspace') -param monitorWorkspaceName string - -@description('Array of principal IDs to assign the Monitoring Metrics Publisher role to') -param principalIds array - -resource monitorWorkspace 'Microsoft.Monitor/accounts@2023-04-03' existing = { - name: monitorWorkspaceName -} - -@description('This is the built-in Monitoring Metrics Publisher role. See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#monitoring-metrics-publisher') -resource monitoringMetricsPublisherRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - scope: subscription() - name: '3913510d-42f4-4e42-8a64-420c390055eb' -} - -resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for principalId in principalIds: { - scope: monitorWorkspace - name: guid(monitorWorkspace.id, principalId, monitoringMetricsPublisherRole.id) - properties: { - roleDefinitionId: monitoringMetricsPublisherRole.id - principalId: principalId - principalType: 'ServicePrincipal' - } -}] diff --git a/.azure/modules/monitor-workspace/main.bicep b/.azure/modules/monitor-workspace/main.bicep deleted file mode 100644 index b24e57d06..000000000 --- a/.azure/modules/monitor-workspace/main.bicep +++ /dev/null @@ -1,73 +0,0 @@ -@description('The prefix used for naming resources to ensure unique names') -param namePrefix string - -@description('The location where the resources will be deployed') -param location string - -@description('Tags to apply to resources') -param tags object - -resource monitorWorkspace 'Microsoft.Monitor/accounts@2023-04-03' = { - name: '${namePrefix}-monitor' - location: location - properties: { - // todo: enable once we have ensured a connection to this monitor workspace https://github.com/digdir/dialogporten/issues/1462 - publicNetworkAccess: 'Enabled' - } - tags: tags -} - -resource containerAppEnvironmentDataCollectionEndpoint 'Microsoft.Insights/dataCollectionEndpoints@2023-03-11' = { - name: '${namePrefix}-cae-dce' - location: location - properties: { - description: 'DCE for Container App Environment' - networkAcls: { - publicNetworkAccess: 'Enabled' - } - } - tags: tags -} - -resource containerAppEnvironmentDataCollectionRule 'Microsoft.Insights/dataCollectionRules@2023-03-11' = { - name: '${namePrefix}-cae-dcr' - location: location - properties: { - description: 'DCR for Container App Environment' - dataCollectionEndpointId: containerAppEnvironmentDataCollectionEndpoint.id - dataSources: { - prometheusForwarder: [ - { - streams: [ - 'Microsoft-PrometheusMetrics' - ] - name: 'PrometheusDataSource' - } - ] - } - destinations: { - monitoringAccounts: [ - { - accountResourceId: monitorWorkspace.id - name: 'MonitoringAccountDestination' - } - ] - } - dataFlows: [ - { - streams: [ - 'Microsoft-PrometheusMetrics' - ] - destinations: [ - 'MonitoringAccountDestination' - ] - } - ] - } - tags: tags -} - -output monitorWorkspaceId string = monitorWorkspace.id -output monitorWorkspaceName string = monitorWorkspace.name -output containerAppEnvironmentMetricsIngestionEndpoint string = containerAppEnvironmentDataCollectionEndpoint.properties.metricsIngestion.endpoint -output containerAppEnvironmentLogsIngestionEndpoint string = containerAppEnvironmentDataCollectionEndpoint.properties.logsIngestion.endpoint diff --git a/.azure/modules/vnet/main.bicep b/.azure/modules/vnet/main.bicep index 8b8182897..c58bc06ee 100644 --- a/.azure/modules/vnet/main.bicep +++ b/.azure/modules/vnet/main.bicep @@ -263,44 +263,6 @@ resource serviceBusNSG 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { tags: tags } -resource monitorNSG 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { - name: '${namePrefix}-monitor-nsg' - location: location - properties: { - securityRules: [ - { - name: 'AllowAnyCustomAnyInbound' - type: 'Microsoft.Network/networkSecurityGroups/securityRules' - properties: { - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 100 - direction: 'Inbound' - } - } - { - name: 'AllowAnyCustomAnyOutbound' - type: 'Microsoft.Network/networkSecurityGroups/securityRules' - properties: { - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: '*' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 100 - direction: 'Outbound' - } - } - ] - } - tags: tags -} - resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = { name: '${namePrefix}-vnet' location: location @@ -372,17 +334,6 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = { } } } - { - name: 'monitorSubnet' - properties: { - addressPrefix: '10.0.6.0/24' - networkSecurityGroup: { - id: monitorNSG.id - } - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } - } ] } tags: tags @@ -411,8 +362,3 @@ output redisSubnetId string = resourceId( virtualNetwork.name, 'redisSubnet' ) -output monitorSubnetId string = resourceId( - 'Microsoft.Network/virtualNetworks/subnets', - virtualNetwork.name, - 'monitorSubnet' -) diff --git a/.env b/.env index 8300930d6..2309e6938 100644 --- a/.env +++ b/.env @@ -5,3 +5,8 @@ POSTGRES_DB=dialogporten DB_CONNECTION_STRING=Server=dialogporten-postgres;Port=5432;Database=${POSTGRES_DB};User ID=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}; COMPOSE_PROJECT_NAME=digdir + +# OTEL +OTEL_NAMESPACE=dialogporten-local +OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 +OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \ No newline at end of file diff --git a/.github/slack-templates/pipeline-failed.json b/.github/slack-templates/pipeline-failed.json index cd02b10a0..43c41db3c 100644 --- a/.github/slack-templates/pipeline-failed.json +++ b/.github/slack-templates/pipeline-failed.json @@ -23,7 +23,7 @@ "type": "section", "text": { "type": "mrkdwn", - "text": "*Job Status:*\n• Infrastructure: ${{ env.INFRA_STATUS }}\n• Apps: ${{ env.APPS_STATUS }}\n• Slack Notifier: ${{ env.SLACK_NOTIFIER_STATUS }}\n• E2E Tests: ${{ env.E2E_TESTS_STATUS }}\n• Performance Tests: ${{ env.PERFORMANCE_TESTS_STATUS }}\n• Schema NPM: ${{ env.SCHEMA_NPM_STATUS }}\n• Publish: ${{ env.PUBLISH_STATUS }}" + "text": "*Job Status:*\n• Infrastructure: ${{ env.INFRA_STATUS }}\n• Apps: ${{ env.APPS_STATUS }}\n• E2E Tests: ${{ env.E2E_TESTS_STATUS }}\n• Performance Tests: ${{ env.PERFORMANCE_TESTS_STATUS }}\n• Schema NPM: ${{ env.SCHEMA_NPM_STATUS }}\n• Publish: ${{ env.PUBLISH_STATUS }}" } }, { diff --git a/.github/workflows/ci-cd-main.yml b/.github/workflows/ci-cd-main.yml index 52a8bd597..d77025508 100644 --- a/.github/workflows/ci-cd-main.yml +++ b/.github/workflows/ci-cd-main.yml @@ -108,22 +108,6 @@ jobs: version: ${{ needs.get-current-version.outputs.version }}-${{ needs.generate-git-short-sha.outputs.gitShortSha }} runMigration: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasMigrationChanges == 'true' }} - deploy-slack-notifier: - name: Deploy slack notifier (test) - needs: [check-for-changes] - if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasSlackNotifierChanges == 'true' }} - uses: ./.github/workflows/workflow-deploy-function.yml - secrets: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - # todo: resolve this automatically, or use tags - AZURE_FUNCTION_APP_NAME: ${{ secrets.AZURE_SLACK_NOTIFIER_FUNCTION_APP_NAME }} - with: - function-app-name: "slack-notifier" - function-project-path: "./src/Digdir.Tool.Dialogporten.SlackNotifier" - environment: test - publish-schema-npm: name: Deploy schema npm package needs: [check-for-changes, get-current-version, generate-git-short-sha, deploy-apps] @@ -156,7 +140,6 @@ jobs: needs: [ deploy-infra, deploy-apps, - deploy-slack-notifier, run-e2e-tests, publish-schema-npm, publish, @@ -168,7 +151,6 @@ jobs: environment: test infra_status: ${{ needs.deploy-infra.result }} apps_status: ${{ needs.deploy-apps.result }} - slack_notifier_status: ${{ needs.deploy-slack-notifier.result }} e2e_tests_status: ${{ needs.run-e2e-tests.result }} schema_npm_status: ${{ needs.publish-schema-npm.result }} publish_status: ${{ needs.publish.result }} diff --git a/.github/workflows/ci-cd-prod.yml b/.github/workflows/ci-cd-prod.yml index 4313c0110..7cf4ca86c 100644 --- a/.github/workflows/ci-cd-prod.yml +++ b/.github/workflows/ci-cd-prod.yml @@ -139,22 +139,6 @@ jobs: secrets: GH_TOKEN: ${{ secrets.RELEASE_VERSION_STORAGE_PAT }} - deploy-slack-notifier: - name: Deploy slack notifier (prod) - needs: [check-for-changes] - if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasSlackNotifierChanges == 'true' }} - uses: ./.github/workflows/workflow-deploy-function.yml - secrets: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - # todo: resolve this automatically, or use tags - AZURE_FUNCTION_APP_NAME: ${{ secrets.AZURE_SLACK_NOTIFIER_FUNCTION_APP_NAME }} - with: - function-app-name: "slack-notifier" - function-project-path: "./src/Digdir.Tool.Dialogporten.SlackNotifier" - environment: prod - # run-e2e-tests: # name: "Run K6 functional end-to-end tests" # # we want the end-to-end tests to be dependent on deployment of infrastructure and apps, but if infrastructure is skipped, we still want to run the tests @@ -174,14 +158,13 @@ jobs: send-slack-message-on-failure: name: Send Slack message on failure - needs: [deploy-infra, deploy-apps, deploy-slack-notifier] + needs: [deploy-infra, deploy-apps] if: ${{ always() && failure() && !cancelled() }} uses: ./.github/workflows/workflow-send-ci-cd-status-slack-message.yml with: environment: prod infra_status: ${{ needs.deploy-infra.result }} apps_status: ${{ needs.deploy-apps.result }} - slack_notifier_status: ${{ needs.deploy-slack-notifier.result }} secrets: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID_FOR_CI_CD_STATUS }} diff --git a/.github/workflows/ci-cd-staging.yml b/.github/workflows/ci-cd-staging.yml index 5091b5238..611839543 100644 --- a/.github/workflows/ci-cd-staging.yml +++ b/.github/workflows/ci-cd-staging.yml @@ -105,22 +105,6 @@ jobs: secrets: GH_TOKEN: ${{ secrets.RELEASE_VERSION_STORAGE_PAT }} - deploy-slack-notifier: - name: Deploy slack notifier (staging) - needs: [check-for-changes] - if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasSlackNotifierChanges == 'true' }} - uses: ./.github/workflows/workflow-deploy-function.yml - secrets: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - # todo: resolve this automatically, or use tags - AZURE_FUNCTION_APP_NAME: ${{ secrets.AZURE_SLACK_NOTIFIER_FUNCTION_APP_NAME }} - with: - function-app-name: "slack-notifier" - function-project-path: "./src/Digdir.Tool.Dialogporten.SlackNotifier" - environment: staging - publish-schema-npm: name: Publish schema npm package needs: [check-for-changes, get-current-version, deploy-apps] @@ -150,14 +134,13 @@ jobs: send-slack-message-on-failure: name: Send Slack message on failure - needs: [deploy-infra, deploy-apps, deploy-slack-notifier, run-e2e-tests, publish-schema-npm, publish] + needs: [deploy-infra, deploy-apps, run-e2e-tests, publish-schema-npm, publish] if: ${{ always() && failure() && !cancelled() }} uses: ./.github/workflows/workflow-send-ci-cd-status-slack-message.yml with: environment: staging infra_status: ${{ needs.deploy-infra.result }} apps_status: ${{ needs.deploy-apps.result }} - slack_notifier_status: ${{ needs.deploy-slack-notifier.result }} e2e_tests_status: ${{ needs.run-e2e-tests.result }} schema_npm_status: ${{ needs.publish-schema-npm.result }} publish_status: ${{ needs.publish.result }} diff --git a/.github/workflows/ci-cd-yt01.yml b/.github/workflows/ci-cd-yt01.yml index 74a8f5df3..7cf989443 100644 --- a/.github/workflows/ci-cd-yt01.yml +++ b/.github/workflows/ci-cd-yt01.yml @@ -107,22 +107,6 @@ jobs: secrets: GH_TOKEN: ${{ secrets.RELEASE_VERSION_STORAGE_PAT }} - deploy-slack-notifier: - name: Deploy slack notifier (yt01) - needs: [check-for-changes] - if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasSlackNotifierChanges == 'true' }} - uses: ./.github/workflows/workflow-deploy-function.yml - secrets: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - # todo: resolve this automatically, or use tags - AZURE_FUNCTION_APP_NAME: ${{ secrets.AZURE_SLACK_NOTIFIER_FUNCTION_APP_NAME }} - with: - function-app-name: "slack-notifier" - function-project-path: "./src/Digdir.Tool.Dialogporten.SlackNotifier" - environment: yt01 - run-e2e-tests: name: "Run K6 functional end-to-end tests" # we want the end-to-end tests to be dependent on deployment of infrastructure and apps, but if infrastructure is skipped, we still want to run the tests @@ -150,15 +134,14 @@ jobs: secrets: TOKEN_GENERATOR_USERNAME: ${{ secrets.TOKEN_GENERATOR_USERNAME }} TOKEN_GENERATOR_PASSWORD: ${{ secrets.TOKEN_GENERATOR_PASSWORD }} - strategy: max-parallel: 1 matrix: - files: + files: - tests/k6/tests/serviceowner/serviceOwnerSearchWithThresholds.js - tests/k6/tests/serviceowner/createDialogWithThresholds.js - tests/k6/tests/enduser/enduserSearchWithThresholds.js - fail-fast: false + fail-fast: false with: environment: yt01 apiVersion: v1 @@ -171,14 +154,13 @@ jobs: send-slack-message-on-failure: name: Send Slack message on failure - needs: [deploy-infra, deploy-apps, deploy-slack-notifier, run-e2e-tests, publish, run-performance-tests] + needs: [deploy-infra, deploy-apps, run-e2e-tests, publish, run-performance-tests] if: ${{ always() && failure() && !cancelled() }} uses: ./.github/workflows/workflow-send-ci-cd-status-slack-message.yml with: environment: yt01 infra_status: ${{ needs.deploy-infra.result }} apps_status: ${{ needs.deploy-apps.result }} - slack_notifier_status: ${{ needs.deploy-slack-notifier.result }} e2e_tests_status: ${{ needs.run-e2e-tests.result }} performance_tests_status: ${{ needs.run-performance-tests.result }} publish_status: ${{ needs.publish.result }} diff --git a/.github/workflows/workflow-deploy-function.yml b/.github/workflows/workflow-deploy-function.yml deleted file mode 100644 index e64632b22..000000000 --- a/.github/workflows/workflow-deploy-function.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Deploy DotNet project to Azure Function App - -on: - workflow_call: - secrets: - AZURE_CLIENT_ID: - required: true - AZURE_TENANT_ID: - required: true - AZURE_SUBSCRIPTION_ID: - required: true - AZURE_FUNCTION_APP_NAME: - required: true - - inputs: - function-app-name: - type: string - required: true - function-project-path: - type: string - required: true - environment: - type: string - required: true - -concurrency: - # If multiple merges to main are performed simultaneously, they will just be queued up. - group: ${{ github.workflow }}-${{ inputs.environment }}-${{ github.ref_name }} - -jobs: - build-and-deploy: - name: Build and deploy ${{ inputs.function-app-name }} to ${{ inputs.environment }} - runs-on: ubuntu-latest - environment: ${{ inputs.environment }} - permissions: - id-token: write - contents: read - steps: - - name: "Checkout GitHub Action" - uses: actions/checkout@v4 - - - name: OIDC Login to Azure Public Cloud - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Setup DotNet Environment - uses: actions/setup-dotnet@v4 - with: - global-json-file: ./global.json - - - name: "Resolve Project Dependencies Using Dotnet" - shell: bash - run: | - pushd './${{ inputs.function-project-path }}' - dotnet build -c Release -o ./output - popd - - - name: "Run Azure Functions Action" - uses: Azure/functions-action@v1 - id: fa - with: - app-name: ${{ secrets.AZURE_FUNCTION_APP_NAME }} - package: "${{ inputs.function-project-path }}/output" - publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }} diff --git a/.github/workflows/workflow-send-ci-cd-status-slack-message.yml b/.github/workflows/workflow-send-ci-cd-status-slack-message.yml index 557f5d1e6..ef613e320 100644 --- a/.github/workflows/workflow-send-ci-cd-status-slack-message.yml +++ b/.github/workflows/workflow-send-ci-cd-status-slack-message.yml @@ -14,10 +14,6 @@ on: type: string description: "Status of the apps deployment job" default: "skipped" - slack_notifier_status: - type: string - description: "Status of the Slack notifier deployment job" - default: "skipped" e2e_tests_status: type: string description: "Status of the end-to-end tests job" @@ -68,7 +64,6 @@ jobs: { echo "INFRA_EMOJI=$(determine_emoji "${{ inputs.infra_status }}")" echo "APPS_EMOJI=$(determine_emoji "${{ inputs.apps_status }}")" - echo "SLACK_NOTIFIER_EMOJI=$(determine_emoji "${{ inputs.slack_notifier_status }}")" echo "E2E_TESTS_EMOJI=$(determine_emoji "${{ inputs.e2e_tests_status }}")" echo "SCHEMA_NPM_EMOJI=$(determine_emoji "${{ inputs.schema_npm_status }}")" echo "PUBLISH_EMOJI=$(determine_emoji "${{ inputs.publish_status }}")" @@ -85,7 +80,6 @@ jobs: # statuses INFRA_STATUS: "${{ steps.status-emojis.outputs.INFRA_EMOJI }}" APPS_STATUS: "${{ steps.status-emojis.outputs.APPS_EMOJI }}" - SLACK_NOTIFIER_STATUS: "${{ steps.status-emojis.outputs.SLACK_NOTIFIER_EMOJI }}" E2E_TESTS_STATUS: "${{ steps.status-emojis.outputs.E2E_TESTS_EMOJI }}" SCHEMA_NPM_STATUS: "${{ steps.status-emojis.outputs.SCHEMA_NPM_EMOJI }}" PUBLISH_STATUS: "${{ steps.status-emojis.outputs.PUBLISH_EMOJI }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c23c3bb..fe7fae707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## [1.44.1](https://github.com/digdir/dialogporten/compare/v1.44.0...v1.44.1) (2025-01-07) + + +### Bug Fixes + +* **ci:** Use correct size for yt01 db ([#1658](https://github.com/digdir/dialogporten/issues/1658)) ([e18e5f7](https://github.com/digdir/dialogporten/commit/e18e5f7e44556e1a4173906303ccef14aeb9de13)) + +## [1.44.0](https://github.com/digdir/dialogporten/compare/v1.43.0...v1.44.0) (2025-01-07) + + +### Features + +* **webapi:** Add ETag to response headers ([#1645](https://github.com/digdir/dialogporten/issues/1645)) ([7a32e60](https://github.com/digdir/dialogporten/commit/7a32e601061b42400aa1c94b61be69ff7c9d0ec9)) + + +### Bug Fixes + +* disable slack notifier ([#1655](https://github.com/digdir/dialogporten/issues/1655)) ([554fc8b](https://github.com/digdir/dialogporten/commit/554fc8b3294c125b0e8561ebcbfe254e75fede1c)) + +## [1.43.0](https://github.com/digdir/dialogporten/compare/v1.42.1...v1.43.0) (2025-01-07) + + +### Features + +* Add additional types to DialogActivity ([#1629](https://github.com/digdir/dialogporten/issues/1629)) ([feb1347](https://github.com/digdir/dialogporten/commit/feb1347c0a79406e0a8f6bb312faad42c8db7eec)) + + +### Bug Fixes + +* **app:** Add dedicated scope and dbcontext to GetSubjectResources ([#1648](https://github.com/digdir/dialogporten/issues/1648)) ([d1040e4](https://github.com/digdir/dialogporten/commit/d1040e41e2b09d1b8e3388ada4790ab1d63c738b)) +* revert azure monitor workspace ([#1624](https://github.com/digdir/dialogporten/issues/1624)) ([d66b155](https://github.com/digdir/dialogporten/commit/d66b155f3e6749466c344ee9aa9319810f65cf6c)) + +## [1.42.1](https://github.com/digdir/dialogporten/compare/v1.42.0...v1.42.1) (2024-12-25) + + +### Bug Fixes + +* **webapi:** Only allow transmissionId on TransmissionOpened activities ([#1631](https://github.com/digdir/dialogporten/issues/1631)) ([80261d1](https://github.com/digdir/dialogporten/commit/80261d18af159acd000c2fa06d6ae351aa681d7d)) + +## [1.42.0](https://github.com/digdir/dialogporten/compare/v1.41.3...v1.42.0) (2024-12-16) + + +### Features + +* **apps:** add otel exporter for graphql, service and web-api ([#1528](https://github.com/digdir/dialogporten/issues/1528)) ([cb9238e](https://github.com/digdir/dialogporten/commit/cb9238ef76188b4dde371e08b7ce597645bcd8b7)) + ## [1.41.3](https://github.com/digdir/dialogporten/compare/v1.41.2...v1.41.3) (2024-12-13) diff --git a/README.md b/README.md index 0135ecec0..2b85f2a90 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ #### Prerequisites -- [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) (see [global.json](global.json) for the currently required version) +- [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) (see [global.json](global.json) for the currently required version) #### Installing Podman (Mac) @@ -34,14 +34,14 @@ brew install docker-compose #### Prerequisites - [Git](https://git-scm.com/download/win) -- [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) +- [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) - [WSL2](https://docs.microsoft.com/en-us/windows/wsl/install) (To install, open a PowerShell admin window and run `wsl --install`) - [Virtual Machine Platform](https://support.microsoft.com/en-us/windows/enable-virtualization-on-windows-11-pcs-c5578302-6e43-4b4b-a449-8ced115f58e1) (Installs with WSL2, see the link above) #### Installing Podman (Windows) 1. Install [Podman Desktop](https://podman.io/getting-started/installation). -. + 2. Start Podman Desktop and follow instructions to install Podman. 3. Follow instructions in Podman Desktop to create and start a Podman machine. @@ -113,6 +113,68 @@ Besides ordinary unit and integration tests, there are test suites for both func See `tests/k6/README.md` for more information. +## Health Checks + +The project includes integrated health checks that are exposed through standard endpoints: +- `/health/startup` - Dependency checks +- `/health/liveness` - Self checks +- `/health/readiness` - Critical service checks +- `/health` - General health status +- `/health/deep` - Comprehensive health check including external services + +These health checks are integrated with Azure Container Apps' health probe system and are used to monitor the application's health status. + +## Observability with OpenTelemetry + +This project uses OpenTelemetry for distributed tracing and metrics collection. The setup includes: + +### Core Features +- Distributed tracing across services +- Runtime and application metrics +- Integration with Azure Monitor/Application Insights +- Support for both OTLP and Azure Monitor exporters +- Automatic instrumentation for: + - ASP.NET Core + - HTTP clients + - Entity Framework Core + - PostgreSQL + - FusionCache + +### Configuration + +OpenTelemetry is configured through environment variables that are automatically provided by Azure Container Apps in production environments: + +```json +{ + "OTEL_SERVICE_NAME": "your-service-name", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://your-collector:4317", + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + "OTEL_RESOURCE_ATTRIBUTES": "key1=value1,key2=value2", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "your-connection-string" +} +``` + +### Local Development + +For local development, the project includes a docker-compose setup with: +- OpenTelemetry Collector +- Grafana +- Other supporting services + +To run the local observability stack: +```bash +podman compose -f docker-compose-otel.yml up +``` + +### Request Filtering + +The telemetry setup includes smart filtering to: +- Exclude health check endpoints from tracing +- Filter out duplicate traces from Azure SDK clients +- Only record relevant HTTP client calls + +For more details about the OpenTelemetry setup, see the `ConfigureTelemetry` method in `AspNetUtilitiesExtensions.cs`. + ## Updating the SDK in global.json When RenovateBot updates `global.json` or base image versions in Dockerfiles, make sure they match. The `global.json` file should always have the same SDK version as the base image in the Dockerfiles. @@ -329,7 +391,7 @@ There is a `ssh-jumper` virtual machine deployed with the infrastructure. This c 2. Using the forwarding utility script: - See [scripts/forward-bash/README.md](scripts/forward-bash/README.md) for a more user-friendly way to establish database connections through SSH. + See [scripts/database-forwarder/README.md](scripts/database-forwarder/README.md) for a more user-friendly way to establish database connections through SSH. ### Applications diff --git a/docker-compose-otel.yml b/docker-compose-otel.yml new file mode 100644 index 000000000..cb8f38aad --- /dev/null +++ b/docker-compose-otel.yml @@ -0,0 +1,45 @@ +services: + # OpenTelemetry Collector + otel-collector: + image: otel/opentelemetry-collector-contrib:0.116.1 + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./local-otel-configuration/otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP http receiver + - "8888:8888" # Prometheus metrics exposed by the collector + - "8889:8889" # Prometheus exporter metrics + depends_on: + jaeger: + condition: service_healthy + + # Jaeger for trace visualization + jaeger: + image: jaegertracing/all-in-one:1.64.0 + ports: + - "16686:16686" # Jaeger UI + - "14250:14250" # Model used by collector + environment: + - COLLECTOR_OTLP_ENABLED=true + + # Prometheus for metrics + prometheus: + image: prom/prometheus:v3.0.1 + volumes: + - ./local-otel-configuration/prometheus.yml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + + # Grafana for metrics visualization + grafana: + image: grafana/grafana:11.4.0 + ports: + - "3000:3000" + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + volumes: + - ./local-otel-configuration/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml + - ./local-otel-configuration/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/dashboards.yml + - ./local-otel-configuration/dashboards:/etc/grafana/provisioning/dashboards diff --git a/docker-compose.yml b/docker-compose.yml index f2a7ec59f..4519148b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ include: - docker-compose-db-redis.yml - docker-compose-cdc.yml + - docker-compose-otel.yml services: dialogporten-webapi-ingress: @@ -14,7 +15,7 @@ services: restart: always dialogporten-webapi: - scale: 2 + scale: 1 build: context: . dockerfile: src/Digdir.Domain.Dialogporten.WebApi/Dockerfile @@ -34,6 +35,10 @@ services: - Serilog__MinimumLevel__Default=Debug - ASPNETCORE_URLS=http://+:8080 - ASPNETCORE_ENVIRONMENT=Development + - OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT} + - OTEL_EXPORTER_OTLP_PROTOCOL=${OTEL_EXPORTER_OTLP_PROTOCOL} + - OTEL_SERVICE_NAME=dialogporten-webapi + - OTEL_RESOURCE_ATTRIBUTES=service.instance.id=dialogporten-webapi,service.namespace=${OTEL_NAMESPACE} volumes: - ./.aspnet/https:/https @@ -70,5 +75,9 @@ services: - Serilog__WriteTo__0__Name=Console - Serilog__MinimumLevel__Default=Debug - ASPNETCORE_ENVIRONMENT=Development + - OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT} + - OTEL_EXPORTER_OTLP_PROTOCOL=${OTEL_EXPORTER_OTLP_PROTOCOL} + - OTEL_SERVICE_NAME=dialogporten-graphql + - OTEL_RESOURCE_ATTRIBUTES=service.instance.id=dialogporten-graphql,service.namespace=${OTEL_NAMESPACE} volumes: - ./.aspnet/https:/https diff --git a/docs/schema/V1/package-lock.json b/docs/schema/V1/package-lock.json index 4aaec0128..a6fefd9ea 100644 --- a/docs/schema/V1/package-lock.json +++ b/docs/schema/V1/package-lock.json @@ -11,10 +11,10 @@ "devDependencies": { "glob": "11.0.0", "graphql-tag": "2.12.6", - "vitest": "2.1.3" + "vitest": "2.1.8" }, "engines": { - "node": "20" + "node": "22" } }, "node_modules/@esbuild/aix-ppc64": { @@ -406,7 +406,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -633,15 +634,15 @@ "dev": true }, "node_modules/@vitest/expect": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz", - "integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.3", - "@vitest/utils": "2.1.3", - "chai": "^5.1.1", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -649,22 +650,21 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz", - "integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.3", + "@vitest/spy": "2.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.11" + "magic-string": "^0.30.12" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.3", - "msw": "^2.3.5", + "msw": "^2.4.9", "vite": "^5.0.0" }, "peerDependenciesMeta": { @@ -677,9 +677,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz", - "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -690,13 +690,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz", - "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.3", + "@vitest/utils": "2.1.8", "pathe": "^1.1.2" }, "funding": { @@ -704,14 +704,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz", - "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.3", - "magic-string": "^0.30.11", + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", "pathe": "^1.1.2" }, "funding": { @@ -719,27 +719,27 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz", - "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.0" + "tinyspy": "^3.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz", - "integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.3", - "loupe": "^3.1.1", + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, "funding": { @@ -775,6 +775,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -804,10 +805,11 @@ } }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -824,6 +826,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" } @@ -884,6 +887,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -900,6 +904,13 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -947,6 +958,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -978,15 +999,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/glob": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", @@ -1072,13 +1084,11 @@ } }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "11.0.0", @@ -1090,9 +1100,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", "dependencies": { @@ -1192,6 +1202,7 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.16" } @@ -1325,9 +1336,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", "dev": true, "license": "MIT" }, @@ -1435,17 +1446,18 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", - "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, "node_modules/tinypool": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", - "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" } @@ -1536,14 +1548,15 @@ } }, "node_modules/vite-node": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", - "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.6", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, @@ -1558,30 +1571,31 @@ } }, "node_modules/vitest": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz", - "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.3", - "@vitest/mocker": "2.1.3", - "@vitest/pretty-format": "^2.1.3", - "@vitest/runner": "2.1.3", - "@vitest/snapshot": "2.1.3", - "@vitest/spy": "2.1.3", - "@vitest/utils": "2.1.3", - "chai": "^5.1.1", - "debug": "^4.3.6", - "magic-string": "^0.30.11", + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", "pathe": "^1.1.2", - "std-env": "^3.7.0", + "std-env": "^3.8.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.0", - "tinypool": "^1.0.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.3", + "vite-node": "2.1.8", "why-is-node-running": "^2.3.0" }, "bin": { @@ -1596,8 +1610,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.3", - "@vitest/ui": "2.1.3", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", "happy-dom": "*", "jsdom": "*" }, diff --git a/docs/schema/V1/package.json b/docs/schema/V1/package.json index b65a007fe..4704869d8 100644 --- a/docs/schema/V1/package.json +++ b/docs/schema/V1/package.json @@ -3,7 +3,7 @@ "version": "1.0.11", "description": "GraphQl schema and OpenAPI spec for Dialogporten", "engines": { - "node": "20" + "node": "22" }, "author": "DigDir", "main": "src/index.js", @@ -16,7 +16,7 @@ "test": "npm run build && vitest run --test-timeout=10000" }, "devDependencies": { - "vitest": "2.1.3", + "vitest": "2.1.8", "glob": "11.0.0", "graphql-tag": "2.12.6" }, diff --git a/docs/schema/V1/swagger.verified.json b/docs/schema/V1/swagger.verified.json index e617e216e..375c25de3 100644 --- a/docs/schema/V1/swagger.verified.json +++ b/docs/schema/V1/swagger.verified.json @@ -83,7 +83,13 @@ "TransmissionOpened", "PaymentMade", "SignatureProvided", - "DialogOpened" + "DialogOpened", + "DialogDeleted", + "DialogRestored", + "SentToSigning", + "SentToFormFill", + "SentToSendIn", + "SentToPayment" ], "type": "string", "x-enumNames": [ @@ -93,7 +99,13 @@ "TransmissionOpened", "PaymentMade", "SignatureProvided", - "DialogOpened" + "DialogOpened", + "DialogDeleted", + "DialogRestored", + "SentToSigning", + "SentToFormFill", + "SentToSendIn", + "SentToPayment" ] }, "DialogsEntitiesTransmissions_DialogTransmissionType": { @@ -315,7 +327,7 @@ "additionalProperties": false, "properties": { "mediaType": { - "description": "Media type of the content (plaintext, Markdown). Can also indicate that the content is embeddable.", + "description": "Media type of the content, this can also indicate that the content is embeddable.\nFor a list of supported media types, see (link TBD).", "type": "string" }, "value": { @@ -5851,7 +5863,12 @@ } } }, - "description": "The UUID of the created the dialog aggregate. A relative URL to the newly created activity is set in the \u0022Location\u0022 header." + "description": "The UUID of the created dialog aggregate. A relative URL to the newly created activity is set in the \u0022Location\u0022 header.", + "headers": { + "Etag": { + "description": "The new UUID ETag of the dialog" + } + } }, "204": { "description": "No Content" @@ -5920,7 +5937,12 @@ ], "responses": { "204": { - "description": "The dialog aggregate was deleted successfully." + "description": "The dialog aggregate was deleted successfully.", + "headers": { + "Etag": { + "description": "The new UUID ETag of the dialog" + } + } }, "400": { "content": { @@ -6076,7 +6098,12 @@ }, "responses": { "204": { - "description": "Patch was successfully applied." + "description": "Patch was successfully applied.", + "headers": { + "Etag": { + "description": "The new UUID ETag of the dialog" + } + } }, "400": { "content": { @@ -6171,7 +6198,12 @@ }, "responses": { "204": { - "description": "The dialog aggregate was updated successfully." + "description": "The dialog aggregate was updated successfully.", + "headers": { + "Etag": { + "description": "The new UUID ETag of the dialog" + } + } }, "400": { "content": { @@ -6469,7 +6501,12 @@ } } }, - "description": "The UUID of the created the dialog activity. A relative URL to the newly created activity is set in the \u0022Location\u0022 header." + "description": "The UUID of the created dialog activity. A relative URL to the newly created activity is set in the \u0022Location\u0022 header.", + "headers": { + "Etag": { + "description": "The new UUID ETag of the dialog" + } + } }, "204": { "description": "No Content" @@ -6832,7 +6869,12 @@ } } }, - "description": "The UUID of the created the dialog transmission. A relative URL to the newly created activity is set in the \u0022Location\u0022 header." + "description": "The UUID of the created dialog transmission. A relative URL to the newly created activity is set in the \u0022Location\u0022 header.", + "headers": { + "Etag": { + "description": "The new UUID ETag of the dialog" + } + } }, "204": { "description": "No Content" diff --git a/local-otel-configuration/dashboards/aspnet-core-metrics.json b/local-otel-configuration/dashboards/aspnet-core-metrics.json new file mode 100644 index 000000000..f60749a2f --- /dev/null +++ b/local-otel-configuration/dashboards/aspnet-core-metrics.json @@ -0,0 +1,176 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "HTTP Request Duration", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "sum(rate(dialogporten_http_server_request_duration_seconds_bucket[$__rate_interval])) by (le)", + "legendFormat": "{{le}}", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.2.0", + "title": "Active Requests", + "type": "gauge", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "dialogporten_http_server_active_requests", + "refId": "A" + } + ] + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "ASP.NET Core Metrics", + "uid": "aspnet-core-metrics", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/local-otel-configuration/dashboards/dashboards.yml b/local-otel-configuration/dashboards/dashboards.yml new file mode 100755 index 000000000..e69de29bb diff --git a/local-otel-configuration/dashboards/http-client-metrics.json b/local-otel-configuration/dashboards/http-client-metrics.json new file mode 100644 index 000000000..56e2f687e --- /dev/null +++ b/local-otel-configuration/dashboards/http-client-metrics.json @@ -0,0 +1,296 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "HTTP Client Active Requests & Connections", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "dialogporten_http_client_active_requests", + "legendFormat": "Active Requests", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "dialogporten_http_client_open_connections", + "legendFormat": "Open Connections", + "refId": "B" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Request Queue Time", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "rate(dialogporten_http_client_request_time_in_queue_seconds_sum[$__rate_interval]) / rate(dialogporten_http_client_request_time_in_queue_seconds_count[$__rate_interval])", + "legendFormat": "Average Queue Time", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "DNS Lookup Duration", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "rate(dialogporten_dns_lookup_duration_seconds_sum[$__rate_interval]) / rate(dialogporten_dns_lookup_duration_seconds_count[$__rate_interval])", + "legendFormat": "Average DNS Lookup Time", + "refId": "A" + } + ] + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "title": "HTTP Client Metrics", + "uid": "http-client-metrics", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/local-otel-configuration/dashboards/runtime-metrics.json b/local-otel-configuration/dashboards/runtime-metrics.json new file mode 100644 index 000000000..eed1ef0c6 --- /dev/null +++ b/local-otel-configuration/dashboards/runtime-metrics.json @@ -0,0 +1,390 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Memory Usage", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "dialogporten_process_runtime_dotnet_gc_heap_size_bytes", + "legendFormat": "Heap Size", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "GC Collections", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "rate(dialogporten_process_runtime_dotnet_gc_collections_count_total[5m])", + "legendFormat": "Gen {{generation}}", + "refId": "A" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Thread Pool", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "dialogporten_process_runtime_dotnet_thread_pool_queue_length", + "legendFormat": "Queue Length", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "dialogporten_process_runtime_dotnet_thread_pool_threads_count", + "legendFormat": "Thread Count", + "refId": "B" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Exceptions & Lock Contentions", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "rate(dialogporten_process_runtime_dotnet_exceptions_count_total[$__rate_interval])", + "legendFormat": "Exceptions/sec", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "expr": "rate(dialogporten_process_runtime_dotnet_monitor_lock_contention_count_total[$__rate_interval])", + "legendFormat": "Lock Contentions/sec", + "refId": "B" + } + ] + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "title": "Runtime Metrics", + "uid": "runtime-metrics", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/local-otel-configuration/grafana-dashboards.yml b/local-otel-configuration/grafana-dashboards.yml new file mode 100644 index 000000000..3712f1164 --- /dev/null +++ b/local-otel-configuration/grafana-dashboards.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'Default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/local-otel-configuration/grafana-datasources.yml b/local-otel-configuration/grafana-datasources.yml new file mode 100644 index 000000000..4139ccba6 --- /dev/null +++ b/local-otel-configuration/grafana-datasources.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true \ No newline at end of file diff --git a/local-otel-configuration/otel-collector-config.yaml b/local-otel-configuration/otel-collector-config.yaml new file mode 100644 index 000000000..97bc0c2b6 --- /dev/null +++ b/local-otel-configuration/otel-collector-config.yaml @@ -0,0 +1,52 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + cors: + allowed_origins: ["*"] + allowed_headers: ["*"] + include_metadata: true + +processors: + batch: + timeout: 1s + send_batch_size: 1024 + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + namespace: "dialogporten" + otlp: + endpoint: "jaeger:4317" + tls: + insecure: true + debug: + verbosity: detailed + sampling_initial: 5 + sampling_thereafter: 200 + +extensions: + health_check: + pprof: + zpages: + +service: + extensions: [health_check, pprof, zpages] + telemetry: + logs: + level: debug + development: true + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp, debug] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus, debug] + logs: + receivers: [otlp] + processors: [batch] + exporters: [debug] diff --git a/local-otel-configuration/prometheus.yml b/local-otel-configuration/prometheus.yml new file mode 100644 index 000000000..58650dae4 --- /dev/null +++ b/local-otel-configuration/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'otel-collector' + static_configs: + - targets: ['otel-collector:8889'] \ No newline at end of file diff --git a/renovate.json b/renovate.json index 74b545b9a..026d030e0 100644 --- a/renovate.json +++ b/renovate.json @@ -17,6 +17,24 @@ "schedule": [ "every 3 months" ] + }, + { + "packagePatterns": [ + "^Npgsql" + ], + "groupName": "Npgsql dependencies" + }, + { + "packagePatterns": [ + "^Microsoft" + ], + "groupName": "Microsoft dependencies" + }, + { + "packagePatterns": [ + "^Serilog" + ], + "groupName": "Serilog dependencies" } ] } diff --git a/src/Digdir.Domain.Dialogporten.Application/Digdir.Domain.Dialogporten.Application.csproj b/src/Digdir.Domain.Dialogporten.Application/Digdir.Domain.Dialogporten.Application.csproj index 55cec0c47..3c661b369 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Digdir.Domain.Dialogporten.Application.csproj +++ b/src/Digdir.Domain.Dialogporten.Application/Digdir.Domain.Dialogporten.Application.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs index c3db7d6a5..58f5bf96c 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs @@ -11,7 +11,8 @@ public sealed class ContentValueDto public List Value { get; set; } = []; /// - /// Media type of the content (plaintext, Markdown). Can also indicate that the content is embeddable. + /// Media type of the content, this can also indicate that the content is embeddable. + /// For a list of supported media types, see (link TBD). /// public string MediaType { get; set; } = MediaTypes.PlainText; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs index 71a28d03f..3aa06d588 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs @@ -21,6 +21,12 @@ internal static class CloudEventTypes nameof(DialogActivityType.Values.PaymentMade) => "dialogporten.dialog.activity.payment-made.v1", nameof(DialogActivityType.Values.SignatureProvided) => "dialogporten.dialog.activity.signature-provided.v1", nameof(DialogActivityType.Values.DialogOpened) => "dialogporten.dialog.activity.dialog-opened.v1", + nameof(DialogActivityType.Values.DialogDeleted) => "dialogporten.dialog.activity.dialog-deleted.v1", + nameof(DialogActivityType.Values.DialogRestored) => "dialogporten.dialog.activity.dialog-restored.v1", + nameof(DialogActivityType.Values.SentToSigning) => "dialogporten.dialog.activity.sent-to-signing.v1", + nameof(DialogActivityType.Values.SentToFormFill) => "dialogporten.dialog.activity.sent-to-form-fill.v1", + nameof(DialogActivityType.Values.SentToSendIn) => "dialogporten.dialog.activity.sent-to-send-in.v1", + nameof(DialogActivityType.Values.SentToPayment) => "dialogporten.dialog.activity.sent-to-payment.v1", _ => throw new ArgumentOutOfRangeException(nameof(eventName), eventName, null) }; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs index 2fee13aa3..09374e635 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommand.cs @@ -21,8 +21,10 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialog public sealed class CreateDialogCommand : CreateDialogDto, IRequest; +public sealed record CreateDialogSuccess(Guid DialogId, Guid Revision); + [GenerateOneOf] -public sealed partial class CreateDialogResult : OneOfBase, DomainError, ValidationError, Forbidden>; +public sealed partial class CreateDialogResult : OneOfBase; internal sealed class CreateDialogCommandHandler : IRequestHandler { @@ -93,7 +95,7 @@ public async Task Handle(CreateDialogCommand request, Cancel await _db.Dialogs.AddAsync(dialog, cancellationToken); var saveResult = await _unitOfWork.SaveChangesAsync(cancellationToken); return saveResult.Match( - success => new Success(dialog.Id), + success => new CreateDialogSuccess(dialog.Id, dialog.Revision), domainError => domainError, concurrencyError => throw new UnreachableException("Should never get a concurrency error when creating a new dialog")); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs index 42a867469..e35be4053 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs @@ -434,11 +434,11 @@ public CreateDialogDialogActivityDtoValidator( .When(x => x.Type != DialogActivityType.Values.Information); RuleFor(x => x.TransmissionId) .Null() - .WithMessage($"A {nameof(DialogActivityType.Values.DialogOpened)} activity cannot reference a transmission.") - .When(x => x.Type == DialogActivityType.Values.DialogOpened); + .WithMessage($"Only activities of type {nameof(DialogActivityType.Values.TransmissionOpened)} can reference a transmission.") + .When(x => x.Type != DialogActivityType.Values.TransmissionOpened); RuleFor(x => x.TransmissionId) .NotEmpty() - .WithMessage($"A {nameof(DialogActivityType.Values.TransmissionOpened)} needs to reference a transmission.") + .WithMessage($"An activity of type {nameof(DialogActivityType.Values.TransmissionOpened)} needs to reference a transmission.") .When(x => x.Type == DialogActivityType.Values.TransmissionOpened); } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs index c64e816d1..9b9250ed6 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs @@ -19,7 +19,9 @@ public sealed class DeleteDialogCommand : IRequest } [GenerateOneOf] -public sealed partial class DeleteDialogResult : OneOfBase; +public sealed partial class DeleteDialogResult : OneOfBase; + +public sealed record DeleteDialogSuccess(Guid Revision); internal sealed class DeleteDialogCommandHandler : IRequestHandler { @@ -70,7 +72,7 @@ public async Task Handle(DeleteDialogCommand request, Cancel .SaveChangesAsync(cancellationToken); return saveResult.Match( - success => success, + success => new DeleteDialogSuccess(dialog.Revision), domainError => throw new UnreachableException("Should never get a domain error when deleting a dialog"), concurrencyError => concurrencyError); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs index fb160b026..4b11eb5aa 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs @@ -19,7 +19,6 @@ using MediatR; using Microsoft.EntityFrameworkCore; using OneOf; -using OneOf.Types; namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Update; @@ -31,7 +30,9 @@ public sealed class UpdateDialogCommand : IRequest } [GenerateOneOf] -public sealed partial class UpdateDialogResult : OneOfBase; +public sealed partial class UpdateDialogResult : OneOfBase; + +public sealed record UpdateDialogSuccess(Guid Revision); internal sealed class UpdateDialogCommandHandler : IRequestHandler { @@ -167,7 +168,7 @@ public async Task Handle(UpdateDialogCommand request, Cancel .SaveChangesAsync(cancellationToken); return saveResult.Match( - success => success, + success => new UpdateDialogSuccess(dialog.Revision), domainError => domainError, concurrencyError => concurrencyError); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommandValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommandValidator.cs index 909d47e7a..f7ea74dff 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommandValidator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommandValidator.cs @@ -400,11 +400,11 @@ public UpdateDialogDialogActivityDtoValidator( .When(x => x.Type != DialogActivityType.Values.Information); RuleFor(x => x.TransmissionId) .Null() - .WithMessage($"A {nameof(DialogActivityType.Values.DialogOpened)} activity cannot reference a transmission.") - .When(x => x.Type == DialogActivityType.Values.DialogOpened); + .WithMessage($"Only activities of type {nameof(DialogActivityType.Values.TransmissionOpened)} can reference a transmission.") + .When(x => x.Type != DialogActivityType.Values.TransmissionOpened); RuleFor(x => x.TransmissionId) .NotEmpty() - .WithMessage($"A {nameof(DialogActivityType.Values.TransmissionOpened)} needs to reference a transmission.") + .WithMessage($"An activity of type {nameof(DialogActivityType.Values.TransmissionOpened)} needs to reference a transmission.") .When(x => x.Type == DialogActivityType.Values.TransmissionOpened); } } diff --git a/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Digdir.Domain.Dialogporten.ChangeDataCapture.csproj b/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Digdir.Domain.Dialogporten.ChangeDataCapture.csproj index 824248115..1bb247ea4 100644 --- a/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Digdir.Domain.Dialogporten.ChangeDataCapture.csproj +++ b/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Digdir.Domain.Dialogporten.ChangeDataCapture.csproj @@ -2,12 +2,12 @@ - + - + diff --git a/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Dockerfile b/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Dockerfile index 9f7e6e9f4..c3d76f09c 100644 --- a/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Dockerfile +++ b/src/Digdir.Domain.Dialogporten.ChangeDataCapture/Dockerfile @@ -1,7 +1,7 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:b4bea3a52a0a77317fa93c5bbdb076623f81e3e2f201078d89914da71318b5d8 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:fe8ceeca5ee197deba95419e3b85c32744970b730ae11645e13f1cb74a848d98 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:3fcf6f1e809c0553f9feb222369f58749af314af6f063f389cbd2f913b4ad556 AS build WORKDIR /src COPY [".editorconfig", "."] diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivityType.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivityType.cs index 344be95c0..41b95e447 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivityType.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivityType.cs @@ -10,12 +10,12 @@ public DialogActivityType(Values id) : base(id) { } public enum Values { /// - /// Refers to a dialog that has been created. + /// Indicates that a dialog has been created. /// DialogCreated = 1, /// - /// Refers to a dialog that has been closed. + /// Indicates that a dialog has been closed. /// DialogClosed = 2, @@ -25,7 +25,7 @@ public enum Values Information = 3, /// - /// Refers to a transmission that has been opened. + /// Indicates that a transmission has been opened. /// TransmissionOpened = 4, @@ -40,8 +40,38 @@ public enum Values SignatureProvided = 6, /// - /// Refers to a dialog that has been opened. + /// Indicates that a dialog has been opened. /// DialogOpened = 7, + + /// + /// Indicates that a dialog has been deleted. + /// + DialogDeleted = 8, + + /// + /// Indicates that a dialog has been restored. + /// + DialogRestored = 9, + + /// + /// Indicates that a dialog has been sent to signing. + /// + SentToSigning = 10, + + /// + /// Indicates that a dialog has been sent to form fill. + /// + SentToFormFill = 11, + + /// + /// Indicates that a dialog has been sent to send in. + /// + SentToSendIn = 12, + + /// + /// Indicates that a dialog has been sent to payment. + /// + SentToPayment = 13, } } diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj b/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj index 47b62ee5d..5eddd36b8 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Digdir.Domain.Dialogporten.GraphQL.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Dockerfile b/src/Digdir.Domain.Dialogporten.GraphQL/Dockerfile index 52dc82647..119952117 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/Dockerfile +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Dockerfile @@ -1,8 +1,8 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:b4bea3a52a0a77317fa93c5bbdb076623f81e3e2f201078d89914da71318b5d8 AS base WORKDIR /app EXPOSE 8080 -FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:fe8ceeca5ee197deba95419e3b85c32744970b730ae11645e13f1cb74a848d98 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:3fcf6f1e809c0553f9feb222369f58749af314af6f063f389cbd2f913b4ad556 AS build WORKDIR /src COPY [".editorconfig", "."] diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs b/src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs index 67beed8bb..b851f63f6 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/OpenTelemetryEventListener.cs @@ -2,7 +2,6 @@ using HotChocolate.Execution; using HotChocolate.Execution.Instrumentation; using Microsoft.AspNetCore.Http.Extensions; -using OpenTelemetry.Trace; namespace Digdir.Domain.Dialogporten.GraphQL; diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs index 7753936ac..5808a8408 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs @@ -68,12 +68,15 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura var thisAssembly = Assembly.GetExecutingAssembly(); - builder.ConfigureTelemetry(); - builder.Services.AddOpenTelemetry() - .WithTracing(tracerProviderBuilder => - { - tracerProviderBuilder.AddSource(DialogportenGraphQLSource); - }); + builder.ConfigureTelemetry((settings, configuration) => + { + settings.ServiceName = configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName; + settings.Endpoint = configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; + settings.Protocol = configuration["OTEL_EXPORTER_OTLP_PROTOCOL"]; + settings.AppInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + settings.ResourceAttributes = configuration["OTEL_RESOURCE_ATTRIBUTES"]; + settings.TraceSources.Add(DialogportenGraphQLSource); + }); builder.Services // Options setup diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs index 4ae95cb71..2f46d07ae 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs @@ -10,7 +10,9 @@ using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions; using Digdir.Domain.Dialogporten.Domain.SubjectResources; using Digdir.Domain.Dialogporten.Infrastructure.Common.Exceptions; +using Digdir.Domain.Dialogporten.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion; @@ -25,8 +27,8 @@ internal sealed class AltinnAuthorizationClient : IAltinnAuthorization private readonly IFusionCache _partiesCache; private readonly IFusionCache _subjectResourcesCache; private readonly IUser _user; - private readonly IDialogDbContext _dialogDbContext; private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; private static readonly JsonSerializerOptions SerializerOptions = new() { @@ -38,16 +40,16 @@ public AltinnAuthorizationClient( HttpClient client, IFusionCacheProvider cacheProvider, IUser user, - IDialogDbContext dialogDbContext, - ILogger logger) + ILogger logger, + IServiceScopeFactory serviceScopeFactory) { _httpClient = client ?? throw new ArgumentNullException(nameof(client)); _pdpCache = cacheProvider.GetCache(nameof(Authorization)) ?? throw new ArgumentNullException(nameof(cacheProvider)); _partiesCache = cacheProvider.GetCache(nameof(AuthorizedPartiesResult)) ?? throw new ArgumentNullException(nameof(cacheProvider)); _subjectResourcesCache = cacheProvider.GetCache(nameof(SubjectResource)) ?? throw new ArgumentNullException(nameof(cacheProvider)); _user = user ?? throw new ArgumentNullException(nameof(user)); - _dialogDbContext = dialogDbContext ?? throw new ArgumentNullException(nameof(dialogDbContext)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); } public async Task GetDialogDetailsAuthorization( @@ -180,10 +182,13 @@ await AuthorizationHelper.CollapseSubjectResources( return dialogSearchAuthorizationResult; } - private async Task> GetAllSubjectResources(CancellationToken cancellationToken) => - await _subjectResourcesCache.GetOrSetAsync(nameof(SubjectResource), async ct - => await _dialogDbContext.SubjectResources.ToListAsync(cancellationToken: ct), + await _subjectResourcesCache.GetOrSetAsync(nameof(SubjectResource), async ct => + { + using var scope = _serviceScopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + return await dbContext.SubjectResources.AsNoTracking().ToListAsync(cancellationToken: ct); + }, token: cancellationToken); private async Task PerformDialogDetailsAuthorization( diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj index 847c03471..311f64dea 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj @@ -3,8 +3,8 @@ - - + + diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/MigrationBundle.dockerfile b/src/Digdir.Domain.Dialogporten.Infrastructure/MigrationBundle.dockerfile index 633bd3398..1cddc7c40 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/MigrationBundle.dockerfile +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/MigrationBundle.dockerfile @@ -1,7 +1,7 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:b4bea3a52a0a77317fa93c5bbdb076623f81e3e2f201078d89914da71318b5d8 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:fe8ceeca5ee197deba95419e3b85c32744970b730ae11645e13f1cb74a848d98 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:3fcf6f1e809c0553f9feb222369f58749af314af6f063f389cbd2f913b4ad556 AS build WORKDIR /src RUN dotnet tool install --global dotnet-ef diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20241222195253_AddAdditionalActivityTypes.Designer.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20241222195253_AddAdditionalActivityTypes.Designer.cs new file mode 100644 index 000000000..523a8413b --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20241222195253_AddAdditionalActivityTypes.Designer.cs @@ -0,0 +1,2194 @@ +// +using System; +using Digdir.Domain.Dialogporten.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Digdir.Domain.Dialogporten.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(DialogDbContext))] + [Migration("20241222195253_AddAdditionalActivityTypes")] + partial class AddAdditionalActivityTypes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Actors.Actor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActorId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ActorName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ActorTypeId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("ActorTypeId"); + + b.ToTable("Actor"); + + b.HasDiscriminator().HasValue("Actor"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Actors.ActorType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("ActorType"); + + b.HasData( + new + { + Id = 1, + Name = "PartyRepresentative" + }, + new + { + Id = 2, + Name = "ServiceOwner" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Attachments.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.ToTable("Attachment"); + + b.HasDiscriminator().HasValue("Attachment"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Attachments.AttachmentUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AttachmentId") + .HasColumnType("uuid"); + + b.Property("ConsumerTypeId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("MediaType") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.HasKey("Id"); + + b.HasIndex("AttachmentId"); + + b.HasIndex("ConsumerTypeId"); + + b.ToTable("AttachmentUrl"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Attachments.AttachmentUrlConsumerType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("AttachmentUrlConsumerType"); + + b.HasData( + new + { + Id = 1, + Name = "Gui" + }, + new + { + Id = 2, + Name = "Api" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.DialogEndUserContext", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("Revision") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("SystemLabelId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("DialogId") + .IsUnique(); + + b.HasIndex("SystemLabelId"); + + b.ToTable("DialogEndUserContext"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.LabelAssignmentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ContextId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("LabelAssignmentLog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.SystemLabel", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("SystemLabel"); + + b.HasData( + new + { + Id = 1, + Name = "Default" + }, + new + { + Id = 2, + Name = "Bin" + }, + new + { + Id = 3, + Name = "Archive" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AuthorizationAttribute") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("DialogId"); + + b.ToTable("DialogApiAction"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiActionEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Deprecated") + .HasColumnType("boolean"); + + b.Property("DocumentationUrl") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("HttpMethodId") + .HasColumnType("integer"); + + b.Property("RequestSchema") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("ResponseSchema") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("SunsetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("Version") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("ActionId"); + + b.HasIndex("HttpMethodId"); + + b.ToTable("DialogApiActionEndpoint"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AuthorizationAttribute") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("HttpMethodId") + .HasColumnType("integer"); + + b.Property("IsDeleteDialogAction") + .HasColumnType("boolean"); + + b.Property("PriorityId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.HasKey("Id"); + + b.HasIndex("DialogId"); + + b.HasIndex("HttpMethodId"); + + b.HasIndex("PriorityId"); + + b.ToTable("DialogGuiAction"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionPriority", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogGuiActionPriority"); + + b.HasData( + new + { + Id = 1, + Name = "Primary" + }, + new + { + Id = 2, + Name = "Secondary" + }, + new + { + Id = 3, + Name = "Tertiary" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("ExtendedType") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("TransmissionId") + .HasColumnType("uuid"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DialogId"); + + b.HasIndex("TransmissionId"); + + b.HasIndex("TypeId"); + + b.ToTable("DialogActivity"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogActivityType"); + + b.HasData( + new + { + Id = 1, + Name = "DialogCreated" + }, + new + { + Id = 2, + Name = "DialogClosed" + }, + new + { + Id = 3, + Name = "Information" + }, + new + { + Id = 4, + Name = "TransmissionOpened" + }, + new + { + Id = 5, + Name = "PaymentMade" + }, + new + { + Id = 6, + Name = "SignatureProvided" + }, + new + { + Id = 7, + Name = "DialogOpened" + }, + new + { + Id = 8, + Name = "DialogDeleted" + }, + new + { + Id = 9, + Name = "DialogRestored" + }, + new + { + Id = 10, + Name = "SentToSigning" + }, + new + { + Id = 11, + Name = "SentToFormFill" + }, + new + { + Id = 12, + Name = "SentToSendIn" + }, + new + { + Id = 13, + Name = "SentToPayment" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.HasIndex("DialogId", "TypeId") + .IsUnique(); + + b.ToTable("DialogContent"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContentType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.PrimitiveCollection("AllowedMediaTypes") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("MaxLength") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("OutputInList") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DialogContentType"); + + b.HasData( + new + { + Id = 1, + AllowedMediaTypes = new[] { "text/plain" }, + MaxLength = 255, + Name = "Title", + OutputInList = true, + Required = true + }, + new + { + Id = 2, + AllowedMediaTypes = new[] { "text/plain" }, + MaxLength = 255, + Name = "SenderName", + OutputInList = true, + Required = false + }, + new + { + Id = 3, + AllowedMediaTypes = new[] { "text/plain" }, + MaxLength = 255, + Name = "Summary", + OutputInList = true, + Required = true + }, + new + { + Id = 4, + AllowedMediaTypes = new[] { "text/plain", "text/markdown" }, + MaxLength = 1023, + Name = "AdditionalInfo", + OutputInList = false, + Required = false + }, + new + { + Id = 5, + AllowedMediaTypes = new[] { "text/plain" }, + MaxLength = 20, + Name = "ExtendedStatus", + OutputInList = true, + Required = false + }, + new + { + Id = 6, + AllowedMediaTypes = new[] { "application/vnd.dialogporten.frontchannelembed+json;type=markdown" }, + MaxLength = 1023, + Name = "MainContentReference", + OutputInList = false, + Required = false + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExtendedStatus") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExternalReference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Org") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .UseCollation("C"); + + b.Property("Party") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .UseCollation("C"); + + b.Property("PrecedingProcess") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Process") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Progress") + .HasColumnType("integer"); + + b.Property("Revision") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ServiceResource") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .UseCollation("C"); + + b.Property("ServiceResourceType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StatusId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("VisibleFrom") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DueAt"); + + b.HasIndex("Org"); + + b.HasIndex("Party"); + + b.HasIndex("Process"); + + b.HasIndex("ServiceResource"); + + b.HasIndex("StatusId"); + + b.HasIndex("UpdatedAt"); + + b.ToTable("Dialog", (string)null); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSearchTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(63) + .HasColumnType("character varying(63)"); + + b.HasKey("Id"); + + b.HasIndex("DialogId", "Value") + .IsUnique(); + + b.ToTable("DialogSearchTag"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSeenLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("EndUserTypeId") + .HasColumnType("integer"); + + b.Property("IsViaServiceOwner") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("DialogId"); + + b.HasIndex("EndUserTypeId"); + + b.ToTable("DialogSeenLog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogStatus", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogStatus"); + + b.HasData( + new + { + Id = 1, + Name = "New" + }, + new + { + Id = 2, + Name = "InProgress" + }, + new + { + Id = 3, + Name = "Draft" + }, + new + { + Id = 4, + Name = "Sent" + }, + new + { + Id = 5, + Name = "RequiresAttention" + }, + new + { + Id = 6, + Name = "Completed" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogUserType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogUserType"); + + b.HasData( + new + { + Id = 0, + Name = "Unknown" + }, + new + { + Id = 1, + Name = "Person" + }, + new + { + Id = 2, + Name = "SystemUser" + }, + new + { + Id = 3, + Name = "ServiceOwner" + }, + new + { + Id = 4, + Name = "ServiceOwnerOnBehalfOfPerson" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TransmissionId") + .HasColumnType("uuid"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.HasIndex("TransmissionId", "TypeId") + .IsUnique(); + + b.ToTable("DialogTransmissionContent"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContentType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.PrimitiveCollection("AllowedMediaTypes") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("MaxLength") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DialogTransmissionContentType"); + + b.HasData( + new + { + Id = 1, + AllowedMediaTypes = new[] { "text/plain" }, + MaxLength = 255, + Name = "Title", + Required = true + }, + new + { + Id = 2, + AllowedMediaTypes = new[] { "text/plain" }, + MaxLength = 255, + Name = "Summary", + Required = true + }, + new + { + Id = 3, + AllowedMediaTypes = new[] { "application/vnd.dialogporten.frontchannelembed+json;type=markdown" }, + MaxLength = 1023, + Name = "ContentReference", + Required = false + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AuthorizationAttribute") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("ExtendedType") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("RelatedTransmissionId") + .HasColumnType("uuid"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DialogId"); + + b.HasIndex("RelatedTransmissionId"); + + b.HasIndex("TypeId"); + + b.ToTable("DialogTransmission"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmissionType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogTransmissionType"); + + b.HasData( + new + { + Id = 1, + Name = "Information" + }, + new + { + Id = 2, + Name = "Acceptance" + }, + new + { + Id = 3, + Name = "Rejection" + }, + new + { + Id = 4, + Name = "Request" + }, + new + { + Id = 5, + Name = "Alert" + }, + new + { + Id = 6, + Name = "Decision" + }, + new + { + Id = 7, + Name = "Submission" + }, + new + { + Id = 8, + Name = "Correction" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Http.HttpVerb", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("HttpVerb"); + + b.HasData( + new + { + Id = 1, + Name = "GET" + }, + new + { + Id = 2, + Name = "POST" + }, + new + { + Id = 3, + Name = "PUT" + }, + new + { + Id = 4, + Name = "PATCH" + }, + new + { + Id = 5, + Name = "DELETE" + }, + new + { + Id = 6, + Name = "HEAD" + }, + new + { + Id = 7, + Name = "OPTIONS" + }, + new + { + Id = 8, + Name = "TRACE" + }, + new + { + Id = 9, + Name = "CONNECT" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.Localization", b => + { + b.Property("LocalizationSetId") + .HasColumnType("uuid"); + + b.Property("LanguageCode") + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4095) + .HasColumnType("character varying(4095)"); + + b.HasKey("LocalizationSetId", "LanguageCode"); + + b.ToTable("Localization"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("LocalizationSet"); + + b.HasDiscriminator().HasValue("LocalizationSet"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.ResourcePolicyInformation.ResourcePolicyInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("MinimumAuthenticationLevel") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("Resource") + .IsUnique(); + + b.ToTable("ResourcePolicyInformation"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.SubjectResources.SubjectResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Resource") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("Resource", "Subject") + .IsUnique(); + + b.ToTable("SubjectResource"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Infrastructure.Persistence.IdempotentNotifications.NotificationAcknowledgement", b => + { + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("NotificationHandler") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AcknowledgedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("EventId", "NotificationHandler"); + + b.HasIndex("EventId"); + + b.ToTable("NotificationAcknowledgement"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("MassTransitInboxState", (string)null); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("MassTransitOutboxMessage", (string)null); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("MassTransitOutboxState", (string)null); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.LabelAssignmentLogActor", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Actors.Actor"); + + b.Property("LabelAssignmentLogId") + .HasColumnType("uuid"); + + b.HasIndex("LabelAssignmentLogId") + .IsUnique(); + + b.HasDiscriminator().HasValue("LabelAssignmentLogActor"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityPerformedByActor", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Actors.Actor"); + + b.Property("ActivityId") + .HasColumnType("uuid"); + + b.HasIndex("ActivityId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogActivityPerformedByActor"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSeenLogSeenByActor", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Actors.Actor"); + + b.Property("DialogSeenLogId") + .HasColumnType("uuid"); + + b.HasIndex("DialogSeenLogId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogSeenLogSeenByActor"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmissionSenderActor", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Actors.Actor"); + + b.Property("TransmissionId") + .HasColumnType("uuid"); + + b.HasIndex("TransmissionId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogTransmissionSenderActor"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogAttachment", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Attachments.Attachment"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.HasIndex("DialogId"); + + b.HasDiscriminator().HasValue("DialogAttachment"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmissionAttachment", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Attachments.Attachment"); + + b.Property("TransmissionId") + .HasColumnType("uuid"); + + b.HasIndex("TransmissionId"); + + b.HasDiscriminator().HasValue("DialogTransmissionAttachment"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Attachments.AttachmentDisplayName", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("AttachmentId") + .HasColumnType("uuid"); + + b.HasIndex("AttachmentId") + .IsUnique(); + + b.HasDiscriminator().HasValue("AttachmentDisplayName"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionPrompt", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("GuiActionId") + .HasColumnType("uuid"); + + b.HasIndex("GuiActionId") + .IsUnique(); + + b.ToTable("LocalizationSet", t => + { + t.Property("GuiActionId") + .HasColumnName("DialogGuiActionPrompt_GuiActionId"); + }); + + b.HasDiscriminator().HasValue("DialogGuiActionPrompt"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionTitle", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("GuiActionId") + .HasColumnType("uuid"); + + b.HasIndex("GuiActionId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogGuiActionTitle"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityDescription", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("ActivityId") + .HasColumnType("uuid"); + + b.HasIndex("ActivityId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogActivityDescription"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContentValue", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("DialogContentId") + .HasColumnType("uuid"); + + b.HasIndex("DialogContentId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogContentValue"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContentValue", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("TransmissionContentId") + .HasColumnType("uuid"); + + b.HasIndex("TransmissionContentId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogTransmissionContentValue"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Actors.Actor", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Actors.ActorType", "ActorType") + .WithMany() + .HasForeignKey("ActorTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ActorType"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Attachments.AttachmentUrl", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Attachments.Attachment", "Attachment") + .WithMany("Urls") + .HasForeignKey("AttachmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Attachments.AttachmentUrlConsumerType", "ConsumerType") + .WithMany() + .HasForeignKey("ConsumerTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Attachment"); + + b.Navigation("ConsumerType"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.DialogEndUserContext", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithOne("DialogEndUserContext") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.DialogEndUserContext", "DialogId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.SystemLabel", "SystemLabel") + .WithMany() + .HasForeignKey("SystemLabelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("SystemLabel"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.LabelAssignmentLog", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.DialogEndUserContext", "Context") + .WithMany("LabelAssignmentLogs") + .HasForeignKey("ContextId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("ApiActions") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Dialog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiActionEndpoint", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", "Action") + .WithMany("Endpoints") + .HasForeignKey("ActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Http.HttpVerb", "HttpMethod") + .WithMany() + .HasForeignKey("HttpMethodId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Action"); + + b.Navigation("HttpMethod"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("GuiActions") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Http.HttpVerb", "HttpMethod") + .WithMany() + .HasForeignKey("HttpMethodId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionPriority", "Priority") + .WithMany() + .HasForeignKey("PriorityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("HttpMethod"); + + b.Navigation("Priority"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("Activities") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", "Transmission") + .WithMany("Activities") + .HasForeignKey("TransmissionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("Transmission"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContent", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("Content") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContentType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Status"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSearchTag", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("SearchTags") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Dialog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSeenLog", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("SeenLog") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogUserType", "EndUserType") + .WithMany() + .HasForeignKey("EndUserTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("EndUserType"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContent", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", "Transmission") + .WithMany("Content") + .HasForeignKey("TransmissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContentType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Transmission"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("Transmissions") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", "RelatedTransmission") + .WithMany("RelatedTransmissions") + .HasForeignKey("RelatedTransmissionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmissionType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("RelatedTransmission"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.Localization", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet", "LocalizationSet") + .WithMany("Localizations") + .HasForeignKey("LocalizationSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LocalizationSet"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.LabelAssignmentLogActor", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.LabelAssignmentLog", "LabelAssignmentLog") + .WithOne("PerformedBy") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.LabelAssignmentLogActor", "LabelAssignmentLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LabelAssignmentLog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityPerformedByActor", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", "Activity") + .WithOne("PerformedBy") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityPerformedByActor", "ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSeenLogSeenByActor", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSeenLog", "DialogSeenLog") + .WithOne("SeenBy") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSeenLogSeenByActor", "DialogSeenLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DialogSeenLog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmissionSenderActor", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", "Transmission") + .WithOne("Sender") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmissionSenderActor", "TransmissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Transmission"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogAttachment", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("Attachments") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Dialog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmissionAttachment", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", "Transmission") + .WithMany("Attachments") + .HasForeignKey("TransmissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Transmission"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Attachments.AttachmentDisplayName", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Attachments.Attachment", "Attachment") + .WithOne("DisplayName") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Attachments.AttachmentDisplayName", "AttachmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Attachment"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionPrompt", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", "GuiAction") + .WithOne("Prompt") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionPrompt", "GuiActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuiAction"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionTitle", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", "GuiAction") + .WithOne("Title") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionTitle", "GuiActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuiAction"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityDescription", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", "Activity") + .WithOne("Description") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityDescription", "ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContentValue", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContent", "DialogContent") + .WithOne("Value") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContentValue", "DialogContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DialogContent"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContentValue", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContent", "TransmissionContent") + .WithOne("Value") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContentValue", "TransmissionContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TransmissionContent"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Attachments.Attachment", b => + { + b.Navigation("DisplayName"); + + b.Navigation("Urls"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.DialogEndUserContext", b => + { + b.Navigation("LabelAssignmentLogs"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities.LabelAssignmentLog", b => + { + b.Navigation("PerformedBy") + .IsRequired(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", b => + { + b.Navigation("Endpoints"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", b => + { + b.Navigation("Prompt"); + + b.Navigation("Title"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", b => + { + b.Navigation("Description"); + + b.Navigation("PerformedBy") + .IsRequired(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Contents.DialogContent", b => + { + b.Navigation("Value") + .IsRequired(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", b => + { + b.Navigation("Activities"); + + b.Navigation("ApiActions"); + + b.Navigation("Attachments"); + + b.Navigation("Content"); + + b.Navigation("DialogEndUserContext") + .IsRequired(); + + b.Navigation("GuiActions"); + + b.Navigation("SearchTags"); + + b.Navigation("SeenLog"); + + b.Navigation("Transmissions"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSeenLog", b => + { + b.Navigation("SeenBy") + .IsRequired(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents.DialogTransmissionContent", b => + { + b.Navigation("Value") + .IsRequired(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.DialogTransmission", b => + { + b.Navigation("Activities"); + + b.Navigation("Attachments"); + + b.Navigation("Content"); + + b.Navigation("RelatedTransmissions"); + + b.Navigation("Sender") + .IsRequired(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet", b => + { + b.Navigation("Localizations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20241222195253_AddAdditionalActivityTypes.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20241222195253_AddAdditionalActivityTypes.cs new file mode 100644 index 000000000..7b2663816 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20241222195253_AddAdditionalActivityTypes.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Digdir.Domain.Dialogporten.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddAdditionalActivityTypes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "DialogActivityType", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 8, "DialogDeleted" }, + { 9, "DialogRestored" }, + { 10, "SentToSigning" }, + { 11, "SentToFormFill" }, + { 12, "SentToSendIn" }, + { 13, "SentToPayment" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "DialogActivityType", + keyColumn: "Id", + keyValue: 8); + + migrationBuilder.DeleteData( + table: "DialogActivityType", + keyColumn: "Id", + keyValue: 9); + + migrationBuilder.DeleteData( + table: "DialogActivityType", + keyColumn: "Id", + keyValue: 10); + + migrationBuilder.DeleteData( + table: "DialogActivityType", + keyColumn: "Id", + keyValue: 11); + + migrationBuilder.DeleteData( + table: "DialogActivityType", + keyColumn: "Id", + keyValue: 12); + + migrationBuilder.DeleteData( + table: "DialogActivityType", + keyColumn: "Id", + keyValue: 13); + } + } +} diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs index 78f3166d4..c1495c887 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -561,6 +561,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) { Id = 7, Name = "DialogOpened" + }, + new + { + Id = 8, + Name = "DialogDeleted" + }, + new + { + Id = 9, + Name = "DialogRestored" + }, + new + { + Id = 10, + Name = "SentToSigning" + }, + new + { + Id = 11, + Name = "SentToFormFill" + }, + new + { + Id = 12, + Name = "SentToSendIn" + }, + new + { + Id = 13, + Name = "SentToPayment" }); }); @@ -607,7 +637,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("integer"); - b.Property("AllowedMediaTypes") + b.PrimitiveCollection("AllowedMediaTypes") .IsRequired() .HasColumnType("text[]"); @@ -980,7 +1010,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("integer"); - b.Property("AllowedMediaTypes") + b.PrimitiveCollection("AllowedMediaTypes") .IsRequired() .HasColumnType("text[]"); diff --git a/src/Digdir.Domain.Dialogporten.Janitor/Digdir.Domain.Dialogporten.Janitor.csproj b/src/Digdir.Domain.Dialogporten.Janitor/Digdir.Domain.Dialogporten.Janitor.csproj index 41b558ee8..1b929c63a 100644 --- a/src/Digdir.Domain.Dialogporten.Janitor/Digdir.Domain.Dialogporten.Janitor.csproj +++ b/src/Digdir.Domain.Dialogporten.Janitor/Digdir.Domain.Dialogporten.Janitor.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/Digdir.Domain.Dialogporten.Janitor/Dockerfile b/src/Digdir.Domain.Dialogporten.Janitor/Dockerfile index 8b0dbc453..0ba540bbd 100644 --- a/src/Digdir.Domain.Dialogporten.Janitor/Dockerfile +++ b/src/Digdir.Domain.Dialogporten.Janitor/Dockerfile @@ -1,7 +1,7 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:b4bea3a52a0a77317fa93c5bbdb076623f81e3e2f201078d89914da71318b5d8 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:fe8ceeca5ee197deba95419e3b85c32744970b730ae11645e13f1cb74a848d98 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:3fcf6f1e809c0553f9feb222369f58749af314af6f063f389cbd2f913b4ad556 AS build WORKDIR /src COPY [".editorconfig", "."] diff --git a/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj b/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj index 5edb017d4..a4b65ddb0 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj +++ b/src/Digdir.Domain.Dialogporten.Service/Digdir.Domain.Dialogporten.Service.csproj @@ -1,7 +1,7 @@ - + diff --git a/src/Digdir.Domain.Dialogporten.Service/Dockerfile b/src/Digdir.Domain.Dialogporten.Service/Dockerfile index 79d760639..dfe7c1653 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Dockerfile +++ b/src/Digdir.Domain.Dialogporten.Service/Dockerfile @@ -1,7 +1,7 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:b4bea3a52a0a77317fa93c5bbdb076623f81e3e2f201078d89914da71318b5d8 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:fe8ceeca5ee197deba95419e3b85c32744970b730ae11645e13f1cb74a848d98 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:3fcf6f1e809c0553f9feb222369f58749af314af6f063f389cbd2f913b4ad556 AS build WORKDIR /src COPY [".editorconfig", "."] diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index 3fffde09d..28a1c3a39 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -51,7 +51,14 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura .AddAzureConfiguration(builder.Environment.EnvironmentName) .AddLocalConfiguration(builder.Environment); - builder.ConfigureTelemetry(); + builder.ConfigureTelemetry((settings, configuration) => + { + settings.ServiceName = configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName; + settings.Endpoint = configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; + settings.Protocol = configuration["OTEL_EXPORTER_OTLP_PROTOCOL"]; + settings.AppInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + settings.ResourceAttributes = configuration["OTEL_RESOURCE_ATTRIBUTES"]; + }); builder.Services .AddAzureAppConfiguration() diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs index 17daba2ee..27a652350 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs @@ -3,6 +3,7 @@ internal static class Constants { internal const string IfMatch = "If-Match"; + internal const string ETag = "Etag"; internal const string Authorization = "Authorization"; internal const string CurrentTokenIssuer = "CurrentIssuer"; internal const int MaxRequestBodySize = 100_000; @@ -10,7 +11,7 @@ internal static class Constants internal static class SwaggerSummary { internal const string ReturnedResult = "Successfully returned the dialog {0}."; - internal const string Created = "The UUID of the created the dialog {0}. A relative URL to the newly created activity is set in the \"Location\" header."; + internal const string Created = "The UUID of the created dialog {0}. A relative URL to the newly created activity is set in the \"Location\" header."; internal const string Deleted = "The dialog {0} was deleted successfully."; internal const string Updated = "The dialog {0} was updated successfully."; internal const string ValidationError = "Validation error occured. See problem details for a list of errors."; diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj b/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj index 590d00b8d..4d3a6285c 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj +++ b/src/Digdir.Domain.Dialogporten.WebApi/Digdir.Domain.Dialogporten.WebApi.csproj @@ -15,16 +15,16 @@ - + - + - - - - + + + + diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Dockerfile b/src/Digdir.Domain.Dialogporten.WebApi/Dockerfile index 1c82387b4..fe5608db5 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Dockerfile +++ b/src/Digdir.Domain.Dialogporten.WebApi/Dockerfile @@ -1,8 +1,8 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:d8f01f752bf9bd3ff630319181a2ccfbeecea4080a1912095a34002f61bfa345 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0@sha256:b4bea3a52a0a77317fa93c5bbdb076623f81e3e2f201078d89914da71318b5d8 AS base WORKDIR /app EXPOSE 8080 -FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:fe8ceeca5ee197deba95419e3b85c32744970b730ae11645e13f1cb74a848d98 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.101@sha256:3fcf6f1e809c0553f9feb222369f58749af314af6f063f389cbd2f913b4ad556 AS build WORKDIR /src COPY [".editorconfig", "."] diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/Common/Headers/HttpResponseHeaderExamples.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/Common/Headers/HttpResponseHeaderExamples.cs new file mode 100644 index 000000000..288ed510d --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/Common/Headers/HttpResponseHeaderExamples.cs @@ -0,0 +1,13 @@ +using Digdir.Domain.Dialogporten.WebApi.Common; +using FastEndpoints; + +namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Headers; + +public static class HttpResponseHeaderExamples +{ + public static ResponseHeader NewDialogETagHeader(int statusCode) + => new(statusCode, Constants.ETag) + { + Description = "The new UUID ETag of the dialog", + }; +} diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs index 85484c662..6f1e9040a 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpoint.cs @@ -65,7 +65,16 @@ await errors.Match( var result = await _sender.Send(updateDialogCommand, ct); await result.Match( - success => SendCreatedAtAsync(new GetActivityQuery { DialogId = dialog.Id, ActivityId = req.Id.Value }, req.Id, cancellation: ct), + success => + { + HttpContext.Response.Headers.Append(Constants.ETag, success.Revision.ToString()); + return SendCreatedAtAsync( + new GetActivityQuery + { + DialogId = dialog.Id, + ActivityId = req.Id.Value + }, req.Id, cancellation: ct); + }, notFound => this.NotFoundAsync(notFound, ct), gone => this.GoneAsync(gone, ct), validationError => this.BadRequestAsync(validationError, ct), diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpointSummary.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpointSummary.cs index 8d005c557..b8f7f8057 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpointSummary.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/Create/CreateDialogActivityEndpointSummary.cs @@ -1,6 +1,7 @@ using Digdir.Domain.Dialogporten.WebApi.Common; using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; +using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Headers; using FastEndpoints; namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.DialogActivities.Create; @@ -18,6 +19,7 @@ The activity is created with the given configuration. For more information see t ResponseExamples[StatusCodes.Status201Created] = "018bb8e5-d9d0-7434-8ec5-569a6c8e01fc"; + ResponseHeaders = [HttpResponseHeaderExamples.NewDialogETagHeader(StatusCodes.Status201Created)]; Responses[StatusCodes.Status201Created] = Constants.SwaggerSummary.Created.FormatInvariant("activity"); Responses[StatusCodes.Status400BadRequest] = Constants.SwaggerSummary.ValidationError; Responses[StatusCodes.Status401Unauthorized] = Constants.SwaggerSummary.ServiceOwnerAuthenticationFailure.FormatInvariant(AuthorizationScope.ServiceProvider); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs index 2018e2d3b..27623c061 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpoint.cs @@ -66,7 +66,13 @@ await errors.Match( var result = await _sender.Send(updateDialogCommand, ct); await result.Match( - success => SendCreatedAtAsync(new GetTransmissionQuery { DialogId = dialog.Id, TransmissionId = req.Id.Value }, req.Id, cancellation: ct), + success => + { + HttpContext.Response.Headers.Append(Constants.ETag, success.Revision.ToString()); + return SendCreatedAtAsync( + new GetTransmissionQuery { DialogId = dialog.Id, TransmissionId = req.Id.Value }, req.Id, + cancellation: ct); + }, notFound => this.NotFoundAsync(notFound, ct), gone => this.GoneAsync(gone, ct), validationError => this.BadRequestAsync(validationError, ct), diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpointSummary.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpointSummary.cs index ecb0430cd..4af3789e3 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpointSummary.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogTransmissions/Create/CreateDialogTransmissionEndpointSummary.cs @@ -1,6 +1,7 @@ using Digdir.Domain.Dialogporten.WebApi.Common; using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; +using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Headers; using FastEndpoints; namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.DialogTransmissions.Create; @@ -18,6 +19,7 @@ The transmission is created with the given configuration. For more information s ResponseExamples[StatusCodes.Status201Created] = "018bb8e5-d9d0-7434-8ec5-569a6c8e01fc"; + ResponseHeaders = [HttpResponseHeaderExamples.NewDialogETagHeader(StatusCodes.Status201Created)]; Responses[StatusCodes.Status201Created] = Constants.SwaggerSummary.Created.FormatInvariant("transmission"); Responses[StatusCodes.Status400BadRequest] = Constants.SwaggerSummary.ValidationError; Responses[StatusCodes.Status401Unauthorized] = Constants.SwaggerSummary.ServiceOwnerAuthenticationFailure.FormatInvariant(AuthorizationScope.ServiceProvider); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpoint.cs index cbc9938e7..cb6f33e38 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpoint.cs @@ -1,5 +1,6 @@ using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Create; using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get; +using Digdir.Domain.Dialogporten.WebApi.Common; using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Extensions; @@ -34,7 +35,12 @@ public override async Task HandleAsync(CreateDialogCommand req, CancellationToke { var result = await _sender.Send(req, ct); await result.Match( - success => SendCreatedAtAsync(new GetDialogQuery { DialogId = success.Value }, success.Value, cancellation: ct), + success => + { + HttpContext.Response.Headers.Append(Constants.ETag, success.Revision.ToString()); + return SendCreatedAtAsync(new GetDialogQuery { DialogId = success.DialogId }, + success.DialogId, cancellation: ct); + }, domainError => this.UnprocessableEntityAsync(domainError, ct), validationError => this.BadRequestAsync(validationError, ct), forbidden => this.ForbiddenAsync(forbidden, ct)); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs index ae324eb51..82f7efb89 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Create/CreateDialogEndpointSummary.cs @@ -1,6 +1,7 @@ using Digdir.Domain.Dialogporten.WebApi.Common; using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; +using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Headers; using FastEndpoints; namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Create; @@ -18,6 +19,7 @@ The dialog is created with the given configuration. For more information see the ResponseExamples[StatusCodes.Status201Created] = "018bb8e5-d9d0-7434-8ec5-569a6c8e01fc"; + ResponseHeaders = [HttpResponseHeaderExamples.NewDialogETagHeader(StatusCodes.Status201Created)]; Responses[StatusCodes.Status201Created] = Constants.SwaggerSummary.Created.FormatInvariant("aggregate"); Responses[StatusCodes.Status400BadRequest] = Constants.SwaggerSummary.ValidationError; Responses[StatusCodes.Status401Unauthorized] = Constants.SwaggerSummary.ServiceOwnerAuthenticationFailure.FormatInvariant(AuthorizationScope.ServiceProvider); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpoint.cs index 40f91368d..d8c6445de 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpoint.cs @@ -36,7 +36,11 @@ public override async Task HandleAsync(DeleteDialogRequest req, CancellationToke var command = new DeleteDialogCommand { Id = req.DialogId, IfMatchDialogRevision = req.IfMatchDialogRevision }; var result = await _sender.Send(command, ct); await result.Match( - success => SendNoContentAsync(ct), + success => + { + HttpContext.Response.Headers.Append(Constants.ETag, success.Revision.ToString()); + return SendNoContentAsync(ct); + }, notFound => this.NotFoundAsync(notFound, ct), gone => this.GoneAsync(gone, ct), forbidden => this.ForbiddenAsync(forbidden, ct), diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpointSummary.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpointSummary.cs index 9125a68de..973b9d1bb 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpointSummary.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Delete/DeleteDialogEndpointSummary.cs @@ -1,6 +1,7 @@ using Digdir.Domain.Dialogporten.WebApi.Common; using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; +using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Headers; using FastEndpoints; namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Delete; @@ -18,6 +19,7 @@ Deletes a given dialog (soft delete). For more information see the documentation Optimistic concurrency control is implemented using the If-Match header. Supply the Revision value from the GetDialog endpoint to ensure that the dialog is not deleted by another request in the meantime. """; + ResponseHeaders = [HttpResponseHeaderExamples.NewDialogETagHeader(StatusCodes.Status204NoContent)]; Responses[StatusCodes.Status204NoContent] = Constants.SwaggerSummary.Deleted.FormatInvariant("aggregate"); Responses[StatusCodes.Status401Unauthorized] = Constants.SwaggerSummary.ServiceOwnerAuthenticationFailure.FormatInvariant(AuthorizationScope.ServiceProvider); Responses[StatusCodes.Status403Forbidden] = Constants.SwaggerSummary.AccessDeniedToDialog.FormatInvariant("delete"); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs index b439574cb..f56fbb7ef 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/PatchDialogsController.cs @@ -60,6 +60,7 @@ public PatchDialogsController(ISender sender, IMapper mapper) [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status412PreconditionFailed)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] + [ProducesResponseHeader(StatusCodes.Status204NoContent, Constants.ETag, "The new UUID ETag of the dialog")] public async Task Patch( [FromRoute] Guid dialogId, [FromHeader(Name = Constants.IfMatch)] Guid? etag, @@ -87,7 +88,11 @@ public async Task Patch( var command = new UpdateDialogCommand { Id = dialogId, IfMatchDialogRevision = etag, Dto = updateDialogDto }; var result = await _sender.Send(command, ct); return result.Match( - success => (IActionResult)NoContent(), + success => + { + HttpContext.Response.Headers.Append(Constants.ETag, success.Revision.ToString()); + return (IActionResult)NoContent(); + }, notFound => NotFound(HttpContext.GetResponseOrDefault(StatusCodes.Status404NotFound, notFound.ToValidationResults())), badRequest => BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest, badRequest.ToValidationResults())), validationFailed => BadRequest(HttpContext.GetResponseOrDefault(StatusCodes.Status400BadRequest, validationFailed.Errors.ToList())), diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/ProducesResponseHeaderAttribute.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/ProducesResponseHeaderAttribute.cs new file mode 100644 index 000000000..77e2d3e12 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/ProducesResponseHeaderAttribute.cs @@ -0,0 +1,16 @@ +namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Patch; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class ProducesResponseHeaderAttribute : Attribute +{ + public ProducesResponseHeaderAttribute(int statusCode, string headerName, string description) + { + HeaderName = headerName; + StatusCode = statusCode; + Description = description; + } + + public string HeaderName { get; } + public int StatusCode { get; } + public string Description { get; } +} diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/ProducesResponseHeaderOperationProcessor.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/ProducesResponseHeaderOperationProcessor.cs new file mode 100644 index 000000000..26ec4387e --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Patch/ProducesResponseHeaderOperationProcessor.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using System.Reflection; +using NSwag; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; + +namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Patch; + +public sealed class ProducesResponseHeaderOperationProcessor : IOperationProcessor +{ + public bool Process(OperationProcessorContext context) + { + var headerAttribute = context.MethodInfo.GetCustomAttribute(); + if (headerAttribute == null) + { + return true; + } + + var statusCode = headerAttribute.StatusCode.ToString(CultureInfo.InvariantCulture); + var response = context.OperationDescription.Operation.Responses[statusCode]; + var header = new OpenApiHeader + { + Description = headerAttribute.Description, + }; + + response.Headers.Add(headerAttribute.HeaderName, header); + return true; + } +} diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpoint.cs index d75486f68..e8b866cca 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpoint.cs @@ -43,7 +43,11 @@ public override async Task HandleAsync(UpdateDialogRequest req, CancellationToke var updateDialogResult = await _sender.Send(command, ct); await updateDialogResult.Match( - success => SendNoContentAsync(ct), + success => + { + HttpContext.Response.Headers.Append(Constants.ETag, success.Revision.ToString()); + return SendNoContentAsync(ct); + }, notFound => this.NotFoundAsync(notFound, ct), gone => this.GoneAsync(gone, ct), validationFailed => this.BadRequestAsync(validationFailed, ct), diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpointSummary.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpointSummary.cs index 055cc3eb7..d6874cf58 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpointSummary.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/Update/UpdateDialogEndpointSummary.cs @@ -1,6 +1,7 @@ using Digdir.Domain.Dialogporten.WebApi.Common; using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; using Digdir.Domain.Dialogporten.WebApi.Common.Extensions; +using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.Common.Headers; using FastEndpoints; namespace Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Update; @@ -15,6 +16,7 @@ Replaces a given dialog with the supplied model. For more information see the do {Constants.SwaggerSummary.OptimisticConcurrencyNote} """; + ResponseHeaders = [HttpResponseHeaderExamples.NewDialogETagHeader(StatusCodes.Status204NoContent)]; Responses[StatusCodes.Status204NoContent] = Constants.SwaggerSummary.Updated.FormatInvariant("aggregate"); Responses[StatusCodes.Status400BadRequest] = Constants.SwaggerSummary.ValidationError; Responses[StatusCodes.Status401Unauthorized] = diff --git a/src/Digdir.Domain.Dialogporten.WebApi/OpenApiDocumentExtensions.cs b/src/Digdir.Domain.Dialogporten.WebApi/OpenApiDocumentExtensions.cs index 9dc187e41..a9d53de3f 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/OpenApiDocumentExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/OpenApiDocumentExtensions.cs @@ -1,4 +1,3 @@ -using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Update; using NJsonSchema; using NSwag; diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs index c02e9f007..7674052f5 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Program.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Program.cs @@ -15,6 +15,7 @@ using Digdir.Domain.Dialogporten.WebApi.Common.Authorization; using Digdir.Domain.Dialogporten.WebApi.Common.Json; using Digdir.Domain.Dialogporten.WebApi.Common.Swagger; +using Digdir.Domain.Dialogporten.WebApi.Endpoints.V1.ServiceOwner.Dialogs.Patch; using Digdir.Library.Utils.AspNet; using FastEndpoints; using FastEndpoints.Swagger; @@ -78,7 +79,14 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura var thisAssembly = Assembly.GetExecutingAssembly(); - builder.ConfigureTelemetry(); + builder.ConfigureTelemetry((settings, configuration) => + { + settings.ServiceName = configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName; + settings.Endpoint = configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]; + settings.Protocol = configuration["OTEL_EXPORTER_OTLP_PROTOCOL"]; + settings.AppInsightsConnectionString = configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + settings.ResourceAttributes = configuration["OTEL_RESOURCE_ATTRIBUTES"]; + }); builder.Services // Options setup @@ -118,6 +126,9 @@ static void BuildAndRun(string[] args, TelemetryConfiguration telemetryConfigura // generic "2" suffix duplicate names get, so we add a "SO" suffix to the serviceowner specific schemas. // This should match the operationIds used for service owners. s.AddServiceOwnerSuffixToSchemas(); + + // Adding ResponseHeaders for PATCH MVC controller + s.OperationProcessors.Add(new ProducesResponseHeaderOperationProcessor()); }; }) .AddControllers(options => options.InputFormatters.Insert(0, JsonPatchInputFormatter.Get())) diff --git a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.json b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.json index ffd6358a4..938115f5a 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.json +++ b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.json @@ -19,4 +19,4 @@ } }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/src/Digdir.Library.Entity.Abstractions/Digdir.Library.Entity.Abstractions.csproj b/src/Digdir.Library.Entity.Abstractions/Digdir.Library.Entity.Abstractions.csproj index cb9c284f0..d9961bfe5 100644 --- a/src/Digdir.Library.Entity.Abstractions/Digdir.Library.Entity.Abstractions.csproj +++ b/src/Digdir.Library.Entity.Abstractions/Digdir.Library.Entity.Abstractions.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Digdir.Library.Entity.Abstractions/Features/Identifiable/IdentifiableExtensions.cs b/src/Digdir.Library.Entity.Abstractions/Features/Identifiable/IdentifiableExtensions.cs index 51db679dd..5c647cc26 100644 --- a/src/Digdir.Library.Entity.Abstractions/Features/Identifiable/IdentifiableExtensions.cs +++ b/src/Digdir.Library.Entity.Abstractions/Features/Identifiable/IdentifiableExtensions.cs @@ -32,11 +32,15 @@ public static Guid CreateVersion7IfDefault(this Guid value) => /// /// Creates a new version 7 UUID. + /// /// /// A new version 7 UUID in big endian format. - public static Guid CreateVersion7() => - // We want Guids in big endian format. The default behavior of Medo is big endian, - // however, the implicit conversion from Medo.Uuid7 to Guid is little endian. - // "matchGuidEndianness" is set to true to ensure big endian. - Uuid7.NewUuid7().ToGuid(matchGuidEndianness: true); + // We want Guids in big endian text representation. + // The default behavior of Medo is bigEndian text representation, + // Setting bigEndian to false for two reasons: + // 1. Use the parameter name explicitly in case the Medo API changes + // 2. Make it clear that we want big endian text representation, which means little endian byte order + public static Guid CreateVersion7(DateTimeOffset? timeStamp = null) => timeStamp is null + ? Uuid7.NewUuid7().ToGuid(bigEndian: false) + : Uuid7.NewUuid7(timeStamp.Value).ToGuid(bigEndian: false); } diff --git a/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs b/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs index c47413c8f..c1388ec0d 100644 --- a/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs +++ b/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesExtensions.cs @@ -1,8 +1,8 @@ -using Azure.Monitor.OpenTelemetry.AspNetCore; using Digdir.Library.Utils.AspNet.HealthChecks; using HealthChecks.UI.Client; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; @@ -10,13 +10,15 @@ using OpenTelemetry.Trace; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using System.Diagnostics; +using Azure.Monitor.OpenTelemetry.Exporter; namespace Digdir.Library.Utils.AspNet; public static class AspNetUtilitiesExtensions { - private const string MassTransitSource = "MassTransit"; - public static IServiceCollection AddAspNetHealthChecks(this IServiceCollection services, Action? configure = null) { var optionsBuilder = services.AddOptions(); @@ -49,41 +51,115 @@ private static WebApplication MapHealthCheckEndpoint(this WebApplication app, st return app; } - public static WebApplicationBuilder ConfigureTelemetry(this WebApplicationBuilder builder) + public static WebApplicationBuilder ConfigureTelemetry( + this WebApplicationBuilder builder, + Action? configure = null) { - builder.Services.AddOpenTelemetry() - .ConfigureResource(resource => resource - .AddService(serviceName: builder.Environment.ApplicationName)) - .WithTracing(tracing => + var settings = new TelemetrySettings(); + configure?.Invoke(settings, builder.Configuration); + + Console.WriteLine($"[OpenTelemetry] Configuring telemetry for service: {settings.ServiceName}"); + + var telemetryBuilder = builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => { - if (builder.Environment.IsDevelopment()) + var resourceBuilder = resource.AddService(serviceName: settings.ServiceName ?? builder.Environment.ApplicationName); + + var resourceAttributes = settings.ResourceAttributes; + if (string.IsNullOrEmpty(resourceAttributes)) return; + + try { - tracing.SetSampler(new AlwaysOnSampler()); + var attributes = resourceAttributes + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(pair => pair.Split('=', 2)) + .Where(parts => parts.Length == 2 && !string.IsNullOrEmpty(parts[0])) + .Select(parts => new KeyValuePair(parts[0].Trim(), parts[1].Trim())); + + foreach (var attribute in attributes) + { + resourceBuilder.AddAttributes([attribute]); + } } + catch (Exception ex) + { + throw new InvalidOperationException( + "Failed to parse OTEL_RESOURCE_ATTRIBUTES. Expected format: key1=value1,key2=value2", + ex + ); + } + }); + + if (!string.IsNullOrEmpty(settings.Endpoint) && !string.IsNullOrEmpty(settings.Protocol)) + { + Console.WriteLine($"[OpenTelemetry] Using endpoint: {settings.Endpoint}"); + Console.WriteLine($"[OpenTelemetry] Using protocol: {settings.Protocol}"); - tracing.AddAspNetCoreInstrumentation(options => + var otlpProtocol = settings.Protocol.ToLowerInvariant() switch + { + "grpc" => OtlpExportProtocol.Grpc, + "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + "http" => OtlpExportProtocol.HttpProtobuf, + _ => throw new ArgumentException($"Unsupported protocol: {settings.Protocol}") + }; + + telemetryBuilder.UseOtlpExporter(otlpProtocol, new Uri(settings.Endpoint)); + + telemetryBuilder + .WithTracing(tracing => { - options.Filter = httpContext => - !httpContext.Request.Path.StartsWithSegments("/health"); + if (builder.Environment.IsDevelopment()) + { + tracing.SetSampler(new AlwaysOnSampler()); + } + + foreach (var source in settings.TraceSources) + { + tracing.AddSource(source); + } + + tracing + .AddAspNetCoreInstrumentation(opts => + { + opts.RecordException = true; + opts.Filter = httpContext => !httpContext.Request.Path.StartsWithSegments("/health"); + }) + .AddHttpClientInstrumentation(o => + { + o.RecordException = true; + o.FilterHttpRequestMessage = _ => + { + var parentActivity = Activity.Current?.Parent; + if (parentActivity != null && parentActivity.Source.Name.Equals("Azure.Core.Http", StringComparison.Ordinal)) + { + return false; + } + return true; + }; + }) + .AddEntityFrameworkCoreInstrumentation() + .AddNpgsql() + .AddFusionCacheInstrumentation(); }); - tracing.AddHttpClientInstrumentation(); - tracing.AddNpgsql(); - tracing.AddSource(MassTransitSource); // MassTransit ActivitySource - }) - .WithMetrics(metrics => + telemetryBuilder.WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation(); - }); + metrics.AddRuntimeInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING"))) - { - builder.Services.AddOpenTelemetry().UseAzureMonitor(); + if (!string.IsNullOrEmpty(settings.AppInsightsConnectionString)) + { + metrics.AddAzureMonitorMetricExporter(options => + { + options.ConnectionString = settings.AppInsightsConnectionString; + }); + } + }); } else { - // Use Application Insights SDK for local development - builder.Services.AddApplicationInsightsTelemetry(); + Console.WriteLine("[OpenTelemetry] OTLP exporter not configured - skipping"); } return builder; diff --git a/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesSettings.cs b/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesSettings.cs index f8e2f7fdb..7cb14248c 100644 --- a/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesSettings.cs +++ b/src/Digdir.Library.Utils.AspNet/AspNetUtilitiesSettings.cs @@ -9,3 +9,21 @@ public sealed class HealthCheckSettings { public List HttpGetEndpointsToCheck { get; set; } = []; } + +public sealed class TelemetrySettings +{ + private const string MassTransitSource = "MassTransit"; + private const string AzureSource = "Azure.*"; + + public string? ServiceName { get; set; } + public string? Endpoint { get; set; } + public string? Protocol { get; set; } + public string? AppInsightsConnectionString { get; set; } + // Expected format: key1=value1,key2=value2 + public string? ResourceAttributes { get; set; } + public HashSet TraceSources { get; set; } = new() + { + AzureSource, + MassTransitSource + }; +} \ No newline at end of file diff --git a/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj b/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj index cab4e5a23..36a45f2a5 100644 --- a/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj +++ b/src/Digdir.Library.Utils.AspNet/Digdir.Library.Utils.AspNet.csproj @@ -10,13 +10,16 @@ - + + + + diff --git a/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs b/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs index 7b0af6a26..4cbd05a9d 100644 --- a/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs +++ b/src/Digdir.Tool.Dialogporten.GenerateFakeData/DialogGenerator.cs @@ -242,7 +242,7 @@ public static List GenerateFakeDialogTransmissions(int? count = DialogTransmissionType.Values? type = null) { return new Faker() - .RuleFor(o => o.Id, _ => Uuid7.NewUuid7().ToGuid(true)) + .RuleFor(o => o.Id, _ => IdentifiableExtensions.CreateVersion7()) .RuleFor(o => o.CreatedAt, f => f.Date.Past()) .RuleFor(o => o.Type, f => type ?? f.PickRandom()) .RuleFor(o => o.Sender, _ => new() { ActorType = ActorType.Values.ServiceOwner }) @@ -262,7 +262,7 @@ public static List GenerateFakeDialogActivities(int? count = null, .Where(x => x != DialogActivityType.Values.TransmissionOpened).ToList(); return new Faker() - .RuleFor(o => o.Id, () => Uuid7.NewUuid7().ToGuid(true)) + .RuleFor(o => o.Id, () => IdentifiableExtensions.CreateVersion7()) .RuleFor(o => o.CreatedAt, f => f.Date.Past()) .RuleFor(o => o.ExtendedType, f => new Uri(f.Internet.UrlWithPath(Uri.UriSchemeHttps))) .RuleFor(o => o.Type, f => type ?? f.PickRandom(activityTypes)) diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Digdir.Domain.Dialogporten.Application.Integration.Tests.csproj b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Digdir.Domain.Dialogporten.Application.Integration.Tests.csproj index 942a0cc2e..bcd82360f 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Digdir.Domain.Dialogporten.Application.Integration.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Digdir.Domain.Dialogporten.Application.Integration.Tests.csproj @@ -14,9 +14,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/Common/Events/DomainEventsTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/Common/Events/DomainEventsTests.cs index 115184cb9..3ac416221 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/Common/Events/DomainEventsTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/Common/Events/DomainEventsTests.cs @@ -11,9 +11,9 @@ using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Delete; using Digdir.Domain.Dialogporten.Domain.Attachments; using Digdir.Domain.Dialogporten.Domain.Dialogs.Events.Activities; +using Digdir.Library.Entity.Abstractions.Features.Identifiable; using MassTransit.Internals; using MassTransit.Testing; -using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils; namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.Common.Events; @@ -32,6 +32,20 @@ static DomainEventsTests() Mapper = mapperConfiguration.CreateMapper(); } + [Fact] + public void All_DialogActivityTypes_Must_Have_A_Mapping_In_CloudEventTypes() + { + // Arrange + var allActivityTypes = Enum.GetValues().ToList(); + + // Act/Assert + allActivityTypes.ForEach(activityType => + { + Action act = () => CloudEventTypes.Get(activityType.ToString()); + act.Should().NotThrow($"all activity types must have a mapping in {nameof(CloudEventTypes)} ({activityType} is missing)"); + }); + } + [Fact] public async Task Creates_CloudEvents_When_Dialog_Created() { @@ -126,7 +140,7 @@ public async Task Creates_CloudEvent_When_Attachments_Updates() { // Arrange var harness = await Application.ConfigureServicesWithMassTransitTestHarness(); - var dialogId = GenerateBigEndianUuidV7(); + var dialogId = IdentifiableExtensions.CreateVersion7(); var createDialogCommand = DialogGenerator.GenerateFakeDialog( id: dialogId, attachments: []); @@ -174,7 +188,7 @@ public async Task Creates_CloudEvents_When_Dialog_Deleted() { // Arrange var harness = await Application.ConfigureServicesWithMassTransitTestHarness(); - var dialogId = GenerateBigEndianUuidV7(); + var dialogId = IdentifiableExtensions.CreateVersion7(); var createDialogCommand = DialogGenerator.GenerateFakeDialog(id: dialogId, attachments: [], activities: []); await Application.Send(createDialogCommand); @@ -204,7 +218,7 @@ public async Task Creates_DialogDeletedEvent_When_Dialog_Purged() { // Arrange var harness = await Application.ConfigureServicesWithMassTransitTestHarness(); - var dialogId = GenerateBigEndianUuidV7(); + var dialogId = IdentifiableExtensions.CreateVersion7(); var createDialogCommand = DialogGenerator.GenerateFakeDialog(id: dialogId, attachments: [], activities: []); await Application.Send(createDialogCommand); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/ActivityLogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/ActivityLogTests.cs index d5495680e..6f8ed9bc5 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/ActivityLogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/ActivityLogTests.cs @@ -19,7 +19,7 @@ public async Task Get_Dialog_ActivityLog_Should_Not_Return_User_Ids_Unhashed() var (_, createCommandResponse) = await GenerateDialogWithActivity(); // Act - var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }); // Assert response.TryPickT0(out var result, out _).Should().BeTrue(); @@ -63,13 +63,17 @@ public async Task Get_ActivityLog_Should_Not_Return_User_Ids_Unhashed() // Arrange var (_, createCommandResponse) = await GenerateDialogWithActivity(); - var getDialogResult = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var getDialogResult = await Application.Send(new GetDialogQuery + { + DialogId = createCommandResponse.AsT0.DialogId + }); + var activityId = getDialogResult.AsT0.Activities.First().Id; // Act var response = await Application.Send(new GetActivityQuery { - DialogId = createCommandResponse.AsT0.Value, + DialogId = createCommandResponse.AsT0.DialogId, ActivityId = activityId }); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/DeletedDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/DeletedDialogTests.cs index 30e3613cc..c1be07e25 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/DeletedDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/DeletedDialogTests.cs @@ -16,7 +16,7 @@ public async Task Fetching_Deleted_Dialog_Should_Return_Gone() var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); var createDialogResponse = await Application.Send(createDialogCommand); - var dialogId = createDialogResponse.AsT0.Value; + var dialogId = createDialogResponse.AsT0.DialogId; var deleteDialogCommand = new DeleteDialogCommand { Id = dialogId }; await Application.Send(deleteDialogCommand); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/SeenLogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/SeenLogTests.cs index ab9cb0bfe..872a2356e 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/SeenLogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/SeenLogTests.cs @@ -20,7 +20,7 @@ public async Task Get_Dialog_SeenLog_Should_Not_Return_User_Ids_Unhashed() var createCommandResponse = await Application.Send(createDialogCommand); // Act - var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }); // Assert response.TryPickT0(out var result, out _).Should().BeTrue(); @@ -42,7 +42,7 @@ public async Task Search_Dialog_SeenLog_Should_Not_Return_User_Ids_Unhashed() var createCommandResponse = await Application.Send(createDialogCommand); // Trigger SeenLog - await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }); // Act var response = await Application.Send(new SearchDialogQuery @@ -70,13 +70,17 @@ public async Task Get_SeenLog_Should_Not_Return_User_Ids_Unhashed() var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); var createCommandResponse = await Application.Send(createDialogCommand); - var triggerSeenLogResponse = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var triggerSeenLogResponse = await Application.Send(new GetDialogQuery + { + DialogId = createCommandResponse.AsT0.DialogId + }); + var seenLogId = triggerSeenLogResponse.AsT0.SeenSinceLastUpdate.Single().Id; // Act var response = await Application.Send(new GetSeenLogQuery { - DialogId = createCommandResponse.AsT0.Value, + DialogId = createCommandResponse.AsT0.DialogId, SeenLogId = seenLogId }); @@ -97,12 +101,12 @@ public async Task Search_SeenLog_Should_Not_Return_User_Ids_Unhashed() var createCommandResponse = await Application.Send(createDialogCommand); // Trigger SeenLog - await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }); // Act var response = await Application.Send(new SearchSeenLogQuery { - DialogId = createCommandResponse.AsT0.Value + DialogId = createCommandResponse.AsT0.DialogId }); // Assert diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs index 9e7323f2c..7fc55434d 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs @@ -5,11 +5,11 @@ using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get; using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common; using Digdir.Domain.Dialogporten.Domain; +using Digdir.Library.Entity.Abstractions.Features.Identifiable; using Digdir.Tool.Dialogporten.GenerateFakeData; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils; namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.ServiceOwner.Dialogs.Commands; @@ -38,8 +38,8 @@ public async Task Cant_Create_Dialog_With_UUIDv4_format() public async Task Cant_Create_Dialog_With_UUIDv7_In_Little_Endian_Format() { // Arrange - // Guid created with Medo, Uuid7.NewUuid7().ToGuid() - var invalidDialogId = Guid.Parse("638e9101-6bc7-7975-b392-ba5c5a528c23"); + // Guid created with Medo, Uuid7.NewUuid7().ToGuid(bigEndian: true) + var invalidDialogId = Guid.Parse("b2ca9301-c371-ab74-a87b-4ee1416b9655"); var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(id: invalidDialogId); @@ -56,7 +56,7 @@ public async Task Cant_Create_Dialog_With_ID_With_Timestamp_In_The_Future() { // Arrange var timestamp = DateTimeOffset.UtcNow.AddSeconds(1); - var invalidDialogId = GenerateBigEndianUuidV7(timestamp); + var invalidDialogId = IdentifiableExtensions.CreateVersion7(timestamp); var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(id: invalidDialogId); @@ -73,7 +73,7 @@ public async Task Create_Dialog_With_ID_With_Timestamp_In_The_Past() { // Arrange var timestamp = DateTimeOffset.UtcNow.AddSeconds(-1); - var validDialogId = GenerateBigEndianUuidV7(timestamp); + var validDialogId = IdentifiableExtensions.CreateVersion7(timestamp); var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(id: validDialogId); @@ -83,14 +83,14 @@ public async Task Create_Dialog_With_ID_With_Timestamp_In_The_Past() // Assert response.TryPickT0(out var success, out _).Should().BeTrue(); success.Should().NotBeNull(); - success.Value.Should().Be(validDialogId); + success.DialogId.Should().Be(validDialogId); } [Fact] public async Task Create_CreatesDialog_WhenDialogIsSimple() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createCommand = DialogGenerator.GenerateSimpleFakeDialog(id: expectedDialogId); // Act @@ -98,14 +98,14 @@ public async Task Create_CreatesDialog_WhenDialogIsSimple() // Assert response.TryPickT0(out var success, out _).Should().BeTrue(); - success.Value.Should().Be(expectedDialogId); + success.DialogId.Should().Be(expectedDialogId); } [Fact] public async Task Create_CreateDialog_WhenDialogIsComplex() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createDialogCommand = DialogGenerator.GenerateFakeDialog(id: expectedDialogId); // Act @@ -113,14 +113,14 @@ public async Task Create_CreateDialog_WhenDialogIsComplex() // Assert result.TryPickT0(out var success, out _).Should().BeTrue(); - success.Value.Should().Be(expectedDialogId); + success.DialogId.Should().Be(expectedDialogId); } [Fact] public async Task Can_Create_Dialog_With_UpdatedAt_Supplied() { // Arrange - var dialogId = GenerateBigEndianUuidV7(); + var dialogId = IdentifiableExtensions.CreateVersion7(); var createdAt = DateTimeOffset.UtcNow.AddYears(-20); var updatedAt = DateTimeOffset.UtcNow.AddYears(-15); var createDialogCommand = DialogGenerator.GenerateFakeDialog(id: dialogId, updatedAt: updatedAt, createdAt: createdAt); @@ -136,7 +136,7 @@ public async Task Can_Create_Dialog_With_UpdatedAt_Supplied() // Assert createDialogResult.TryPickT0(out var dialogCreatedSuccess, out _).Should().BeTrue(); - dialogCreatedSuccess.Value.Should().Be(dialogId); + dialogCreatedSuccess.DialogId.Should().Be(dialogId); getDialogQuery.Should().NotBeNull(); getDialogResponse.TryPickT0(out var dialog, out _).Should().BeTrue(); @@ -387,7 +387,7 @@ public async Task Cannot_Create_Title_Content_With_Embeddable_Html_MediaType_Wit public async Task Can_Create_MainContentRef_Content_With_Embeddable_Html_MediaType_With_Correct_Scope() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(id: expectedDialogId); createDialogCommand.Content.MainContentReference = new ContentValueDto { @@ -408,6 +408,20 @@ public async Task Can_Create_MainContentRef_Content_With_Embeddable_Html_MediaTy // Assert response.TryPickT0(out var success, out _).Should().BeTrue(); success.Should().NotBeNull(); - success.Value.Should().Be(expectedDialogId); + success.DialogId.Should().Be(expectedDialogId); + } + + [Fact] + public async Task CreateDialogCommand_Should_Return_Revision() + { + // Arrange + var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); + + // Act + var response = await Application.Send(createDialogCommand); + + // Assert + response.TryPickT0(out var success, out _).Should().BeTrue(); + success.Revision.Should().NotBeEmpty(); } } diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/DeleteDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/DeleteDialogTests.cs index a225fc9a3..459033a6b 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/DeleteDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/DeleteDialogTests.cs @@ -18,7 +18,7 @@ public async Task Deleting_Dialog_Should_Set_DeletedAt() var createDialogResponse = await Application.Send(createDialogCommand); // Act - var dialogId = createDialogResponse.AsT0.Value; + var dialogId = createDialogResponse.AsT0.DialogId; var deleteDialogCommand = new DeleteDialogCommand { Id = dialogId }; await Application.Send(deleteDialogCommand); @@ -38,7 +38,7 @@ public async Task Updating_Deleted_Dialog_Should_Return_EntityDeleted() var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); var createDialogResponse = await Application.Send(createDialogCommand); - var dialogId = createDialogResponse.AsT0.Value; + var dialogId = createDialogResponse.AsT0.DialogId; var deleteDialogCommand = new DeleteDialogCommand { Id = dialogId }; await Application.Send(deleteDialogCommand); @@ -58,4 +58,26 @@ public async Task Updating_Deleted_Dialog_Should_Return_EntityDeleted() entityDeleted.Should().NotBeNull(); entityDeleted.Message.Should().Contain(dialogId.ToString()); } + + [Fact] + public async Task DeleteDialogCommand_Should_Return_New_Revision() + { + // Arrange + var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); + var createDialogResponse = await Application.Send(createDialogCommand); + + var dialogId = createDialogResponse.AsT0.DialogId; + var oldRevision = createDialogResponse.AsT0.Revision; + + // Act + var deleteDialogCommand = new DeleteDialogCommand { Id = dialogId }; + var deleteDialogResponse = await Application.Send(deleteDialogCommand); + + // Assert + deleteDialogResponse.TryPickT0(out var success, out _).Should().BeTrue(); + success.Should().NotBeNull(); + success.Revision.Should().NotBeEmpty(); + success.Revision.Should().NotBe(oldRevision); + } + } diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/PurgeDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/PurgeDialogTests.cs index f1da5c248..22adc5938 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/PurgeDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/PurgeDialogTests.cs @@ -2,9 +2,9 @@ using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; +using Digdir.Library.Entity.Abstractions.Features.Identifiable; using Digdir.Tool.Dialogporten.GenerateFakeData; using FluentAssertions; -using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils; namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.ServiceOwner.Dialogs.Commands; @@ -15,7 +15,7 @@ public class PurgeDialogTests(DialogApplication application) : ApplicationCollec public async Task Purge_RemovesDialog_FromDatabase() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createCommand = DialogGenerator.GenerateFakeDialog(id: expectedDialogId); var createResponse = await Application.Send(createCommand); createResponse.TryPickT0(out _, out _).Should().BeTrue(); @@ -41,7 +41,7 @@ public async Task Purge_RemovesDialog_FromDatabase() public async Task Purge_ReturnsConcurrencyError_OnIfMatchDialogRevisionMismatch() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createCommand = DialogGenerator.GenerateFakeDialog(id: expectedDialogId); var createResponse = await Application.Send(createCommand); createResponse.TryPickT0(out _, out _).Should().BeTrue(); @@ -58,7 +58,7 @@ public async Task Purge_ReturnsConcurrencyError_OnIfMatchDialogRevisionMismatch( public async Task Purge_ReturnsNotFound_OnNonExistingDialog() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createCommand = DialogGenerator.GenerateFakeDialog(id: expectedDialogId); await Application.Send(createCommand); var purgeCommand = new PurgeDialogCommand { DialogId = expectedDialogId }; diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/UpdateDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/UpdateDialogTests.cs index d8e752f60..debac9c8c 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/UpdateDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/UpdateDialogTests.cs @@ -16,12 +16,42 @@ namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.S [Collection(nameof(DialogCqrsCollectionFixture))] public class UpdateDialogTests(DialogApplication application) : ApplicationCollectionFixture(application) { + [Fact] + public async Task UpdateDialogCommand_Should_Return_New_Revision() + { + // Arrange + var createCommandResponse = await Application.Send(DialogGenerator.GenerateSimpleFakeDialog()); + + var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }; + var getDialogDto = await Application.Send(getDialogQuery); + var oldRevision = getDialogDto.AsT0.Revision; + + var mapper = Application.GetMapper(); + var updateDialogDto = mapper.Map(getDialogDto.AsT0); + + // Update something + updateDialogDto.Progress++; + + // Act + var updateResponse = await Application.Send(new UpdateDialogCommand + { + Id = createCommandResponse.AsT0.DialogId, + Dto = updateDialogDto + }); + + // Assert + updateResponse.TryPickT0(out var success, out _).Should().BeTrue(); + success.Should().NotBeNull(); + success.Revision.Should().NotBeEmpty(); + success.Revision.Should().NotBe(oldRevision); + } + [Fact] public async Task Cannot_Include_Old_Activities_To_UpdateCommand() { // Arrange var (_, createCommandResponse) = await GenerateDialogWithActivity(); - var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }; + var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }; var getDialogDto = await Application.Send(getDialogQuery); var mapper = Application.GetMapper(); @@ -39,7 +69,11 @@ public async Task Cannot_Include_Old_Activities_To_UpdateCommand() }); // Act - var updateResponse = await Application.Send(new UpdateDialogCommand { Id = createCommandResponse.AsT0.Value, Dto = updateDialogDto }); + var updateResponse = await Application.Send(new UpdateDialogCommand + { + Id = createCommandResponse.AsT0.DialogId, + Dto = updateDialogDto + }); // Assert updateResponse.TryPickT5(out var domainError, out _).Should().BeTrue(); @@ -56,7 +90,7 @@ public async Task Cannot_Include_Old_Transmissions_In_UpdateCommand() createDialogCommand.Transmissions.Add(existingTransmission); var createCommandResponse = await Application.Send(createDialogCommand); - var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }; + var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }; var getDialogDto = await Application.Send(getDialogQuery); var mapper = Application.GetMapper(); @@ -76,7 +110,11 @@ public async Task Cannot_Include_Old_Transmissions_In_UpdateCommand() }); // Act - var updateResponse = await Application.Send(new UpdateDialogCommand { Id = createCommandResponse.AsT0.Value, Dto = updateDialogDto }); + var updateResponse = await Application.Send(new UpdateDialogCommand + { + Id = createCommandResponse.AsT0.DialogId, + Dto = updateDialogDto + }); // Assert updateResponse.TryPickT5(out var domainError, out _).Should().BeTrue(); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/ActivityLogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/ActivityLogTests.cs index 6b899cff3..a509ec024 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/ActivityLogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/ActivityLogTests.cs @@ -20,7 +20,7 @@ public async Task Get_Dialog_ActivityLog_Should_Return_User_Ids_Unhashed() var (_, createCommandResponse) = await GenerateDialogWithActivity(); // Act - var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }); // Assert response.TryPickT0(out var result, out _).Should().BeTrue(); @@ -64,13 +64,17 @@ public async Task Get_ActivityLog_Should_Return_User_Ids_Unhashed() // Arrange var (_, createCommandResponse) = await GenerateDialogWithActivity(); - var getDialogResult = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var getDialogResult = await Application.Send(new GetDialogQuery + { + DialogId = createCommandResponse.AsT0.DialogId + }); + var activityId = getDialogResult.AsT0.Activities.First().Id; // Act var response = await Application.Send(new GetActivityQuery { - DialogId = createCommandResponse.AsT0.Value, + DialogId = createCommandResponse.AsT0.DialogId, ActivityId = activityId }); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/GetDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/GetDialogTests.cs index 2493b064f..6bd67650a 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/GetDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/GetDialogTests.cs @@ -1,8 +1,8 @@ using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get; using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common; +using Digdir.Library.Entity.Abstractions.Features.Identifiable; using Digdir.Tool.Dialogporten.GenerateFakeData; using FluentAssertions; -using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils; namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.ServiceOwner.Dialogs.Queries; @@ -15,13 +15,13 @@ public GetDialogTests(DialogApplication application) : base(application) { } public async Task Get_ReturnsSimpleDialog_WhenDialogExists() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(id: expectedDialogId); var createCommandResponse = await Application.Send(createDialogCommand); // Act - var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }); // Assert response.TryPickT0(out var result, out _).Should().BeTrue(); @@ -36,12 +36,12 @@ public async Task Get_ReturnsSimpleDialog_WhenDialogExists() public async Task Get_ReturnsDialog_WhenDialogExists() { // Arrange - var expectedDialogId = GenerateBigEndianUuidV7(); + var expectedDialogId = IdentifiableExtensions.CreateVersion7(); var createCommand = DialogGenerator.GenerateFakeDialog(id: expectedDialogId); var createCommandResponse = await Application.Send(createCommand); // Act - var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }); + var response = await Application.Send(new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }); // Assert response.TryPickT0(out var result, out _).Should().BeTrue(); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/SeenLogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/SeenLogTests.cs index 49502e2a0..0068177f9 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/SeenLogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Queries/SeenLogTests.cs @@ -21,10 +21,13 @@ public async Task Get_Dialog_SeenLog_Should_Return_User_Ids_Unhashed() var createCommandResponse = await Application.Send(createDialogCommand); // Call EndUser API to trigger SeenLog - await Application.Send(new GetDialogQueryEndUser { DialogId = createCommandResponse.AsT0.Value }); + await Application.Send(new GetDialogQueryEndUser { DialogId = createCommandResponse.AsT0.DialogId }); // Act - var response = await Application.Send(new GetDialogQueryServiceOwner { DialogId = createCommandResponse.AsT0.Value }); + var response = await Application.Send(new GetDialogQueryServiceOwner + { + DialogId = createCommandResponse.AsT0.DialogId + }); // Assert response.TryPickT0(out var result, out _).Should().BeTrue(); @@ -45,7 +48,7 @@ public async Task Search_Dialog_SeenLog_Should_Return_User_Ids_Unhashed() var createCommandResponse = await Application.Send(createDialogCommand); // Trigger SeenLog - await Application.Send(new GetDialogQueryEndUser { DialogId = createCommandResponse.AsT0.Value }); + await Application.Send(new GetDialogQueryEndUser { DialogId = createCommandResponse.AsT0.DialogId }); // Act var response = await Application.Send(new SearchDialogQuery @@ -73,13 +76,17 @@ public async Task Get_SeenLog_Should_Return_User_Ids_Unhashed() var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); var createCommandResponse = await Application.Send(createDialogCommand); - var triggerSeenLogResponse = await Application.Send(new GetDialogQueryEndUser { DialogId = createCommandResponse.AsT0.Value }); + var triggerSeenLogResponse = await Application.Send(new GetDialogQueryEndUser + { + DialogId = createCommandResponse.AsT0.DialogId + }); + var seenLogId = triggerSeenLogResponse.AsT0.SeenSinceLastUpdate.Single().Id; // Act var response = await Application.Send(new GetSeenLogQuery { - DialogId = createCommandResponse.AsT0.Value, + DialogId = createCommandResponse.AsT0.DialogId, SeenLogId = seenLogId }); @@ -100,12 +107,12 @@ public async Task Search_SeenLog_Should_Return_User_Ids_Unhashed() var createCommandResponse = await Application.Send(createDialogCommand); // Trigger SeenLog - await Application.Send(new GetDialogQueryEndUser { DialogId = createCommandResponse.AsT0.Value }); + await Application.Send(new GetDialogQueryEndUser { DialogId = createCommandResponse.AsT0.DialogId }); // Act var response = await Application.Send(new SearchSeenLogQuery { - DialogId = createCommandResponse.AsT0.Value + DialogId = createCommandResponse.AsT0.DialogId }); // Assert diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/NotificationCondition/NotificationConditionTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/NotificationCondition/NotificationConditionTests.cs index 9c13b255c..90aa57c76 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/NotificationCondition/NotificationConditionTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/NotificationCondition/NotificationConditionTests.cs @@ -40,7 +40,7 @@ public async Task SendNotification_Should_Be_True_When_Conditions_Are_Met( var response = await Application.Send(createDialogCommand); response.TryPickT0(out var dialogId, out _); - var notificationConditionQuery = CreateNotificationConditionQuery(dialogId.Value, activityType, conditionType); + var notificationConditionQuery = CreateNotificationConditionQuery(dialogId.DialogId, activityType, conditionType); if (activityType is DialogActivityType.Values.TransmissionOpened) { @@ -93,11 +93,11 @@ public async Task Gone_Should_Be_Returned_When_Dialog_Is_Deleted() var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); var response = await Application.Send(createDialogCommand); - response.TryPickT0(out var dialogId, out _); + response.TryPickT0(out var success, out _); - await Application.Send(new DeleteDialogCommand { Id = dialogId.Value }); + await Application.Send(new DeleteDialogCommand { Id = success.DialogId }); - var notificationConditionQuery = CreateNotificationConditionQuery(dialogId.Value); + var notificationConditionQuery = CreateNotificationConditionQuery(success.DialogId); // Act var queryResult = await Application.Send(notificationConditionQuery); @@ -106,7 +106,7 @@ public async Task Gone_Should_Be_Returned_When_Dialog_Is_Deleted() queryResult.TryPickT3(out var deleted, out _); queryResult.IsT3.Should().BeTrue(); deleted.Should().NotBeNull(); - deleted.Message.Should().Contain(dialogId.Value.ToString()); + deleted.Message.Should().Contain(success.DialogId.ToString()); } private static NotificationConditionQuery CreateNotificationConditionQuery(Guid dialogId, diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/CreateTransmissionTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/CreateTransmissionTests.cs index 27cfeb4a9..0b52be4c7 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/CreateTransmissionTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/CreateTransmissionTests.cs @@ -4,9 +4,9 @@ using Digdir.Domain.Dialogporten.Domain; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions.Contents; +using Digdir.Library.Entity.Abstractions.Features.Identifiable; using Digdir.Tool.Dialogporten.GenerateFakeData; using FluentAssertions; -using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils; namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.ServiceOwner.Transmissions.Commands; @@ -19,7 +19,7 @@ public CreateTransmissionTests(DialogApplication application) : base(application public async Task Can_Create_Simple_Transmission() { // Arrange - var dialogId = GenerateBigEndianUuidV7(); + var dialogId = IdentifiableExtensions.CreateVersion7(); var createCommand = DialogGenerator.GenerateSimpleFakeDialog(id: dialogId); var transmission = DialogGenerator.GenerateFakeDialogTransmissions(1)[0]; @@ -30,7 +30,7 @@ public async Task Can_Create_Simple_Transmission() // Assert response.TryPickT0(out var success, out _).Should().BeTrue(); - success.Value.Should().Be(dialogId); + success.DialogId.Should().Be(dialogId); var transmissionEntities = await Application.GetDbEntities(); transmissionEntities.Should().HaveCount(1); transmissionEntities.First().DialogId.Should().Be(dialogId); @@ -41,10 +41,10 @@ public async Task Can_Create_Simple_Transmission() public async Task Can_Create_Transmission_With_Embeddable_Content() { // Arrange - var dialogId = GenerateBigEndianUuidV7(); + var dialogId = IdentifiableExtensions.CreateVersion7(); var createCommand = DialogGenerator.GenerateSimpleFakeDialog(id: dialogId); - var transmissionId = GenerateBigEndianUuidV7(); + var transmissionId = IdentifiableExtensions.CreateVersion7(); var transmission = DialogGenerator.GenerateFakeDialogTransmissions(1)[0]; const string contentUrl = "https://example.com/transmission"; @@ -62,7 +62,7 @@ public async Task Can_Create_Transmission_With_Embeddable_Content() // Assert response.TryPickT0(out var success, out _).Should().BeTrue(); - success.Value.Should().Be(dialogId); + success.DialogId.Should().Be(dialogId); var transmissionEntities = await Application.GetDbEntities(); transmissionEntities.Should().HaveCount(1); diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/UpdateTransmissionTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/UpdateTransmissionTests.cs index 261e36056..3e2c51000 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/UpdateTransmissionTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Transmissions/Commands/UpdateTransmissionTests.cs @@ -3,9 +3,9 @@ using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common; using Digdir.Domain.Dialogporten.Domain.Actors; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions; +using Digdir.Library.Entity.Abstractions.Features.Identifiable; using Digdir.Tool.Dialogporten.GenerateFakeData; using FluentAssertions; -using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils; namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.ServiceOwner.Transmissions.Commands; @@ -24,7 +24,7 @@ public async Task Can_Create_Simple_Transmission_In_Update() createDialogCommand.Transmissions.Add(existingTransmission); var createCommandResponse = await Application.Send(createDialogCommand); - var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }; + var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }; var getDialogDto = await Application.Send(getDialogQuery); var mapper = Application.GetMapper(); @@ -36,7 +36,7 @@ public async Task Can_Create_Simple_Transmission_In_Update() // Act var updateResponse = await Application.Send(new UpdateDialogCommand { - Id = createCommandResponse.AsT0.Value, + Id = createCommandResponse.AsT0.DialogId, Dto = updateDialogDto }); @@ -58,7 +58,7 @@ public async Task Can_Update_Related_Transmission_With_Null_Id() createDialogCommand.Transmissions.Add(existingTransmission); var createCommandResponse = await Application.Send(createDialogCommand); - var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }; + var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }; var getDialogDto = await Application.Send(getDialogQuery); var mapper = Application.GetMapper(); @@ -76,7 +76,7 @@ public async Task Can_Update_Related_Transmission_With_Null_Id() // Act var updateResponse = await Application.Send(new UpdateDialogCommand { - Id = createCommandResponse.AsT0.Value, + Id = createCommandResponse.AsT0.DialogId, Dto = updateDialogDto }); @@ -97,7 +97,7 @@ public async Task Cannot_Include_Old_Transmissions_In_UpdateCommand() createDialogCommand.Transmissions.Add(existingTransmission); var createCommandResponse = await Application.Send(createDialogCommand); - var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.Value }; + var getDialogQuery = new GetDialogQuery { DialogId = createCommandResponse.AsT0.DialogId }; var getDialogDto = await Application.Send(getDialogQuery); var mapper = Application.GetMapper(); @@ -110,7 +110,7 @@ public async Task Cannot_Include_Old_Transmissions_In_UpdateCommand() // Act var updateResponse = await Application.Send(new UpdateDialogCommand { - Id = createCommandResponse.AsT0.Value, + Id = createCommandResponse.AsT0.DialogId, Dto = updateDialogDto }); @@ -122,7 +122,7 @@ public async Task Cannot_Include_Old_Transmissions_In_UpdateCommand() private static TransmissionDto UpdateDialogDialogTransmissionDto() => new() { - Id = GenerateBigEndianUuidV7(), + Id = IdentifiableExtensions.CreateVersion7(), Type = DialogTransmissionType.Values.Information, Sender = new() { ActorType = ActorType.Values.ServiceOwner }, Content = new() diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/UUIDv7Utils.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/UUIDv7Utils.cs deleted file mode 100644 index 040fac197..000000000 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/UUIDv7Utils.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Medo; - -namespace Digdir.Domain.Dialogporten.Application.Integration.Tests; - -public static class UuiDv7Utils -{ - public static Guid GenerateBigEndianUuidV7(DateTimeOffset? timeStamp = null) => timeStamp is null ? - Uuid7.NewUuid7().ToGuid(matchGuidEndianness: true) - : Uuid7.NewUuid7(timeStamp.Value).ToGuid(matchGuidEndianness: true); -} diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj index 002f7ea39..3302f4db8 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Digdir.Domain.Dialogporten.Application.Unit.Tests.csproj @@ -12,10 +12,10 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/ServiceOwner/Activities/ActivityValidatorTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/ServiceOwner/Activities/ActivityValidatorTests.cs new file mode 100644 index 000000000..03e5bcb68 --- /dev/null +++ b/tests/Digdir.Domain.Dialogporten.Application.Unit.Tests/Features/V1/ServiceOwner/Activities/ActivityValidatorTests.cs @@ -0,0 +1,62 @@ +using AutoMapper; +using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Localizations; +using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Common.Actors; +using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Create; +using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Update; +using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; +using Digdir.Library.Entity.Abstractions.Features.Identifiable; +using Digdir.Tool.Dialogporten.GenerateFakeData; +using FluentAssertions; + +using UpdateActivityDto = + Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Update.ActivityDto; +using CreateActivityDto = + Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Commands.Create.ActivityDto; + +namespace Digdir.Domain.Dialogporten.Application.Unit.Tests.Features.V1.ServiceOwner.Activities; + +public class ActivityValidatorTests +{ + public static IEnumerable ActivityTypes() => + from DialogActivityType.Values activityType in Enum.GetValues(typeof(DialogActivityType.Values)) + select new object[] { activityType, }; + + [Theory, MemberData(nameof(ActivityTypes))] + public void Only_TransmissionOpened_Is_Allowed_To_Set_TransmissionId( + DialogActivityType.Values activityType) + { + // Arrange + var mapper = new MapperConfiguration(cfg => cfg.CreateMap()).CreateMapper(); + + var activity = DialogGenerator.GenerateFakeDialogActivity(type: activityType); + activity.TransmissionId = IdentifiableExtensions.CreateVersion7(); + + var localizationValidator = new LocalizationDtosValidator(); + var actorValidator = new ActorValidator(); + + var createValidator = new CreateDialogDialogActivityDtoValidator(localizationValidator, actorValidator); + var updateValidator = new UpdateDialogDialogActivityDtoValidator(localizationValidator, actorValidator); + + // Act + var createValidation = createValidator.Validate(activity); + var updateValidation = updateValidator.Validate(mapper.Map(activity)); + + // Assert + if (activityType == DialogActivityType.Values.TransmissionOpened) + { + createValidation.IsValid.Should().BeTrue(); + updateValidation.IsValid.Should().BeTrue(); + } + else + { + createValidation.IsValid.Should().BeFalse(); + updateValidation.IsValid.Should().BeFalse(); + + createValidation.Errors.Should().ContainSingle(); + updateValidation.Errors.Should().ContainSingle(); + + createValidation.Errors.First().ErrorMessage.Should().Contain("TransmissionOpened"); + updateValidation.Errors.First().ErrorMessage.Should().Contain("TransmissionOpened"); + } + } +} diff --git a/tests/Digdir.Domain.Dialogporten.Architecture.Tests/Digdir.Domain.Dialogporten.Architecture.Tests.csproj b/tests/Digdir.Domain.Dialogporten.Architecture.Tests/Digdir.Domain.Dialogporten.Architecture.Tests.csproj index c7b365548..562ae9ec8 100644 --- a/tests/Digdir.Domain.Dialogporten.Architecture.Tests/Digdir.Domain.Dialogporten.Architecture.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.Architecture.Tests/Digdir.Domain.Dialogporten.Architecture.Tests.csproj @@ -8,12 +8,15 @@ - + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj b/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj index aef560cd8..b25d6949f 100644 --- a/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests/Digdir.Domain.Dialogporten.GraphQl.Integration.Tests.csproj @@ -13,9 +13,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests.csproj b/tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests.csproj index e14283c63..8b8965207 100644 --- a/tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests/Digdir.Domain.Dialogporten.GraphQl.Unit.Tests.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests.csproj b/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests.csproj index 56450e1c5..2d9e8c357 100644 --- a/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests.csproj @@ -8,7 +8,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj b/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj index 71af55a0b..451698da9 100644 --- a/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj +++ b/tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests/Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj @@ -8,9 +8,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj b/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj index 6d0a9ac52..58045c4a6 100644 --- a/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj +++ b/tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests/Digdir.Tool.Dialogporten.SlackNotifier.Tests.csproj @@ -8,7 +8,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/version.txt b/version.txt index 021b2b85f..d724e4390 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.41.3 +1.44.1