Daniel Schädler: QuickTipp: Terraform sicher in Azure erstellen

In diesem Artikel gehe ich darauf ein, wie man die Ressourcen mit Terraform in Azure in einer Kadenz von 5 Minuten provisionieren und wieder abbauen kann. Dieses Szenario kann durchaus bei BDD-Test vorkommen, bei deinen Ad-Hoc eine Testinfrastruktur hochgefahren werden muss.

Vorausetzungen

Ausgangslage

Währen der Provisionierung in der Pipeline, kann es immer wieder vokommen, dass bereits eine Ressource noch vorhanden sei, wenn diese rasch wieder erstellt werden muss. Dann sieht man folgende Fehlermeldung:

    │ Error: waiting for creation/update of Server: (Name "sqldbserver" / Resource Group "rg-switzerland"): Code="NameAlreadyExists" Message="The name 'sqldbserver.database.windows.net' already exists. Choose a different name."
    │
    │   with azurerm_mssql_server.sqlsrv,
    │   on main.tf line 52, in resource "azurerm_mssql_server" "sqlsrv":
    │   52: resource "azurerm_mssql_server" "sqlsrv" {

Ärgerlich, wenn man sich darauf verlassen möchte, dass die Test-Umgebung immer gleich aufgebaut werden soll.

Die Lösung

Um diesem Problem Herr zu werden, reicht es wenn man den Ressourcen einen zufälligen Namenszusatz vergibt. Dies kann mit dem Terraform Integer eine einfache Abhilfe geschaffen werden. Dazu braucht es im Terraform Script nur folgende Ressource:

    resource "random_integer" "salt"{
        min = 1
        max = 99
    }

Der Umstand, dass Zahlen zwischen 1 und 99 generiert werden, in einer Zufälligkeit, lässt die Wahrscheinlichkeit, dass eine Ressource bereits besteht und des zu einem Fehler kommt, minimieren.

Das Anlegen einer Ressource-Gruppe mit zufälligem Namenssuffix würde dann wie folgt aussehen.

# Create a resource group
    resource "azurerm_resource_group" "rg" {
      name     = "rg-swiss-${random_integer.salt.result}"
      location = "switzerlandnorth"  
    }

Mit einem Testscript dass fünf mal durchläuft mit einer realistischen Pause von 5min während den Ausführungen hat keinen Fehler ausgegeben.

    $totalseconds = 0;
    $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch
    
    for ($index = 0; $index -lt 5; $index++) {
        $stopwatch.Reset()
        $stopwatch.Start()
    
        $executable = "terraform.exe"
            
        $initargument = "init"
        $plangargument = "plan -out testplan"
        $applyargument = "apply -auto-approve testplan"
        $destroyargeument = "destroy -auto-approve"
        
        Start-Process $executable -ArgumentList $initargument -Wait -NoNewWindow
        Start-Process $executable -ArgumentList $plangargument -Wait -NoNewWindow
        Start-Process $executable -ArgumentList $applyargument -Wait -NoNewWindow
        Start-Process $executable -ArgumentList $destroyargeument -Wait -NoNewWindow
    
        $stopwatch.Stop()
        $totalseconds += $stopwatch.Elapsed.TotalSeconds
        Start-Sleep -Seconds 480
    }
    
    Write-Host "Verstrichene Zeit $totalseconds"

Natürlich kann die Dauer mit der aktuellen Verfügbarkeit von Azure Dienste zusammenhängen. Bei der Vergabe der Namen für die Ressourcen ist es immer ratsam die Azure API der einzelnen Ressourcen zu konsultieren um keinen Fehler in der Länge des Namens zu generieren. Denn im Gegensatz zu früher, wo Namen noch wichtig waren, sind es heute nur noch Ressourcen, die nicht mehr für eine lange Existenz bestimmt sind, in Zeiten von DevOps Praktiken.

Fazit

Mit dieser Lösung kann sichergestellt werden, dass man sich Umgehungslösungen baut, die dann nur einen kleinen Zeitraum funktionieren. Ich hoffe der Artikel hat gefallen.

Daniel Schädler: Quickstart: Bereitstellung statischer Webseite auf Azure

In diesem Artikel möchte ich die Schritte für das Veröffentlichen einer statischen Webseite, zum Beispiel einer "Landing Page" mit Terraform und Azure zeigen.

Voraussetzung

  • Ein Azure Konto ist eingerichtet.
  • Die Azure Cli Tools müssen für das jeweilige Zielsystem installiert sein.
  • Terraform ist installiert und konfiguriert für den Zugriff auf Azure.

Vorgehen

Folgende Schritte werden in der Terraform ausgeführt, damit eine statische Webseite auf Azure veröffentlicht werden kann.

StorageAccount und statische WebApp erstellen

Im ersten Schritt wird ein StorageAccount erstellt.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}

  subscription_id = "YOUR SUBSCRIPTION"
  client_id       = "YOU APPLICATION ID"
  client_secret   = "YOUR APPLICATION SECRET"
  tenant_id       = "YOUR TENANT ID"
}

resource "azurerm_resource_group" "rg" {
  name = "terrfaform-playground"
  # Westeurope da statische Webseiten in der Schweiz
  # noch nicht verfügbar sind.
  location = "westeurope"
}

resource "azurerm_storage_account" "storage" {
  account_tier = "Standard"
  account_kind = "StorageV2"
  account_replication_type = "LRS"
  location = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  name = "schaedldstorage"  
  allow_nested_items_to_be_public = true
  static_website {
    index_document = "index.html"
  }
}

Die Befehle terraform init, terraform plan -out sampleplan, terraform apply sampleplan und terraform destroy (In Produktion eher vorsichtig damit umgehen) ausgeführt. Diese sind durchgängig durch das ganze Beispiel immer wieder anzuwenden.

Terraform init


terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "3.0.0"...
- Installing hashicorp/azurerm v3.0.0...
- Installed hashicorp/azurerm v3.0.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Terraform plan

     terraform plan -out simpleplan

Terraform used the selected providers to generate the following execution plan. Resource actions are    
indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "westeurope"
      + name     = "terrfaform-playground"
    }

  # azurerm_static_site.website will be created
  + resource "azurerm_static_site" "website" {
      + api_key             = (known after apply)
      + default_host_name   = (known after apply)
      + id                  = (known after apply)
      + location            = "westeurope"
      + name                = "sample-web-app"
      + resource_group_name = "terrfaform-playground"
      + sku_size            = "Free"
      + sku_tier            = "Free"
    }

  # azurerm_storage_account.storage will be created
  + resource "azurerm_storage_account" "storage" {
      + access_tier                       = (known after apply)
      + account_kind                      = "StorageV2"
      + account_replication_type          = "LRS"
      + account_tier                      = "Standard"
      + allow_nested_items_to_be_public   = true
      + enable_https_traffic_only         = true
      + id                                = (known after apply)
      + infrastructure_encryption_enabled = false
      + is_hns_enabled                    = false
      + large_file_share_enabled          = (known after apply)
      + location                          = "westeurope"
      + min_tls_version                   = "TLS1_2"
      + name                              = "schaedldstorage"
      + nfsv3_enabled                     = false
      + primary_access_key                = (sensitive value)
      + primary_blob_connection_string    = (sensitive value)
      + primary_blob_endpoint             = (known after apply)
      + primary_blob_host                 = (known after apply)
      + primary_connection_string         = (sensitive value)
      + primary_dfs_endpoint              = (known after apply)
      + primary_dfs_host                  = (known after apply)
      + primary_file_endpoint             = (known after apply)
      + primary_file_host                 = (known after apply)
      + primary_location                  = (known after apply)
      + primary_queue_endpoint            = (known after apply)
      + primary_queue_host                = (known after apply)
      + primary_table_endpoint            = (known after apply)
      + primary_table_host                = (known after apply)
      + primary_web_endpoint              = (known after apply)
      + primary_web_host                  = (known after apply)
      + queue_encryption_key_type         = "Service"
      + resource_group_name               = "terrfaform-playground"
      + secondary_access_key              = (sensitive value)
      + secondary_blob_connection_string  = (sensitive value)
      + secondary_blob_endpoint           = (known after apply)
      + secondary_blob_host               = (known after apply)
      + secondary_connection_string       = (sensitive value)
      + secondary_dfs_endpoint            = (known after apply)
      + secondary_dfs_host                = (known after apply)
      + secondary_file_endpoint           = (known after apply)
      + secondary_file_host               = (known after apply)
      + secondary_location                = (known after apply)
      + secondary_queue_endpoint          = (known after apply)
      + secondary_queue_host              = (known after apply)
      + secondary_table_endpoint          = (known after apply)
      + secondary_table_host              = (known after apply)
      + secondary_web_endpoint            = (known after apply)
      + secondary_web_host                = (known after apply)
      + shared_access_key_enabled         = true
      + table_encryption_key_type         = "Service"

      + blob_properties {
          + change_feed_enabled      = (known after apply)
          + default_service_version  = (known after apply)
          + last_access_time_enabled = (known after apply)
          + versioning_enabled       = (known after apply)

          + container_delete_retention_policy {
              + days = (known after apply)
            }

          + cors_rule {
              + allowed_headers    = (known after apply)
              + allowed_methods    = (known after apply)
              + allowed_origins    = (known after apply)
              + exposed_headers    = (known after apply)
              + max_age_in_seconds = (known after apply)
            }

          + delete_retention_policy {
              + days = (known after apply)
            }
        }

      + network_rules {
          + bypass                     = (known after apply)
          + default_action             = (known after apply)
          + ip_rules                   = (known after apply)
          + virtual_network_subnet_ids = (known after apply)

          + private_link_access {
              + endpoint_resource_id = (known after apply)
              + endpoint_tenant_id   = (known after apply)
            }
        }

      + queue_properties {
          + cors_rule {
              + allowed_headers    = (known after apply)
              + allowed_methods    = (known after apply)
              + allowed_origins    = (known after apply)
              + exposed_headers    = (known after apply)
              + max_age_in_seconds = (known after apply)
            }

          + hour_metrics {
              + enabled               = (known after apply)
              + include_apis          = (known after apply)
              + retention_policy_days = (known after apply)
              + version               = (known after apply)
            }

          + logging {
              + delete                = (known after apply)
              + read                  = (known after apply)
              + retention_policy_days = (known after apply)
              + version               = (known after apply)
              + write                 = (known after apply)
            }

          + minute_metrics {
              + enabled               = (known after apply)
              + include_apis          = (known after apply)
              + retention_policy_days = (known after apply)
              + version               = (known after apply)
            }
        }

      + routing {
          + choice                      = (known after apply)
          + publish_internet_endpoints  = (known after apply)
          + publish_microsoft_endpoints = (known after apply)
        }

      + share_properties {
          + cors_rule {
              + allowed_headers    = (known after apply)
              + allowed_methods    = (known after apply)
              + allowed_origins    = (known after apply)
              + exposed_headers    = (known after apply)
              + max_age_in_seconds = (known after apply)
            }

          + retention_policy {
              + days = (known after apply)
            }

          + smb {
              + authentication_types            = (known after apply)
              + channel_encryption_type         = (known after apply)
              + kerberos_ticket_encryption_type = (known after apply)
              + versions                        = (known after apply)
            }
        }
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Terraform apply


terraform apply sampleplan    
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 0s [id=/subscriptions/YOUR SUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_storage_account.storage: Creating...
azurerm_storage_account.storage: Still creating... [11s elapsed]
azurerm_storage_account.storage: Still creating... [21s elapsed]
azurerm_storage_account.storage: Creation complete after 22s [id=/subscriptions/YOUR SUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]
azurerm_static_site.website: Creating...
azurerm_static_site.website: Creation complete after 3s [id=/subscriptions/YOUR SUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Web/staticSites/sample-web-app]        

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

In Azure ist der StorageAccount erstellt worden.

Azure: StorageAccount erstellt.

Nun können die weiteren Elemente hinzugefügt werden. Gemäss der Anleitung für das Hosten von statischen Webseiten wird noch überprüft ob die Konfiguration von Terraform mit der dokumentierten übereinstimmt.

Azure: Statische Webseite aktiviert.

Hochladen der Landing Page

Azure bietet nicht die Möglichkeit ein Objekt direkt mit Terraform im StorageAccount zu erstellen, sodass ein anderer Weg zur Publizierung gewählt werden muss. Hierzu kann einer der drei dokumentierten Wege gewählt werden:

  • Über das Portal (wenig Automatisierungspotential)
  • Über die Azure Cli Tools
  • Über die Powershell

Das Powershell script ist schnell erläutert:

$storageAccount = Get-AzStorageAccount -Name "schaedldstorage" -ResourceGroupName "terrfaform-playground"
$storageAccountContext = $storageAccount.Context
Set-AzStorageBlobContent -Context $storageAccountContext -Blob "index.html" -File "..\content\index.html" -Container `$web -Properties @{ ContentType="text/html; charset=utf-8;"}

Beim Erstellen des StorageAccounts, wird ein Container automatisch mit dem Namen $web angelegt. Diesen kann man dann für das Hosten verwenden (das Script kopiert die Datei in diesen Container.)

Azure: Web Container

Fazit

Mir nur wenig Aufwand, kann ein erster Kontaktpunkt zu einer neuen Firma auf Azure bereitgestellt werden. Dies ist nur ein Beispiel und hat noch keine Sicherheitsfunktionen aktiviert (vgl. Hosting a static Webseite in Azure Storage). Jedoch ist es weniger simple das Ganze, wie in AWS nur mit Terraform zu bewerkstelligen.

Daniel Schädler: Quickstart mit Azure und Terraform

Was braucht es dazu?

Folgende Voraussetzungen müssen gegeben sein:

  1. Erstellen einer app im Azure Portal.
  2. Kopieren der Schlüssel
  3. Verifizierung des Zugriffes mit den Azure Cli Tools.
  4. Terraform muss installiert sein.

Erstellen einer App im Azure Portal

Um automatisiert Ressourcen auf Azure erstellen zu können, muss vorgängig eine App im Active Directory erstellt werden.

  1. Im Azure Portal Active Directory, App Registrierung auswählen.
Azure: App Registrierung
  1. Nun wählt man neue Registrierung hinzufügen.
Azure: App Registrierung hinzufügen.
  1. Anschliessend im Menü Zertifikate und Geheimnisse ein neuer Geheimer Clientschlüssel erstellen ein.
Azure: Geheimer Schlüssel erstellen.
  1. Nun ist es wichtig, beide Schlüssel zu notieren.
  1. Nun muss noch die Client Id aufgeschrieben werden. Diese findet man in der App Registrierungs-Übersicht.
Azure: Schlüssel erstellen.

Diese können, wenn man sich mit den Azure Cli Tools einmal angemeldet hat mit folgendem Befehl herausgefunden werden:

az account list
  1. Nun muss über das Abonnement und die Zugriffsteuerung der erstellten App eine Rolle zugewiesen werden, damit diese funktioniert. In diesem Beispiel ist die Rolle "Mitwirkender" verwendet worden (Hängt vom Anwendungsfall in der Firma ab, welche tatsächliche Rolle vergeben wird.) Dies geschieht über "Rollenzuweisung hinzufügen".
Azure: Rollenzuweisung hinzufügen.
  1. Anschliessend muss man die zuvor erstellte App hinzufügen. Diese kann mittels Suchfeld gesucht und hinzugefügt werden.
Azure: Applikation hinzufügen.

Verifizierung des Zugriffes mit den Azure Cli Tools

Als erstes müssen die Azure Cli Tools bereits installiert sein.

Sobald die Azure Cli Tools installiert sind, kann man sich mit dem Service Principal versuchen anzumelden.

PS C:\Git-Repos\blogposts> az login --service-principal -u "YOUR APP ID"  -p "APP ID SECRET" --tenant "YOUR TENANT ID"
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "YOUR TENANT",
    "id": "YOUR SUBSCRIPTION",
    "isDefault": true,
    "managedByTenants": [],
    "name": "Pay-As-You-Go",
    "state": "Enabled",
    "tenantId": "YOUR TENANT",
    "user": {
      "name": "YOUR APP ID",
      "type": "servicePrincipal"
    }
  }
]
PS C:\Git-Repos\blogposts> 

Terraform SetUp

Damit Terraform funktioniert, müssen die zuvor heruntergeladenen Schlüssel eingetragen werden. Diese Konfiguration sieht dann wie folgt aus:

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}

  subscription_id = "ABONNEMENT ID"
  client_id       = "ZUVOR ERSTELLTE APPLIKATIONS ID"
  client_secret   = "ZUVOR ERSTELLTES GEHEIMNIS IN DER APP"
  tenant_id       = "TENANT ID"
}

Damit auch hier überprüft werden kann ob die Verbindung mit Azure funktioniert kann auch ein StorageAccount erstellt werden mit den Terraform Ressourcen.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}

  subscription_id = "YOURSUBSCRIPTION"
  client_id       = "APP ID"
  client_secret   = "SECRET ID"
  tenant_id       = "YOUR TENANT"
}

resource "azurerm_resource_group" "rg" {
  name = "terrfaform-playground"
  location = "switzerlandnorth"
}

resource "azurerm_storage_account" "storage" {
  account_tier = "Standard"
  account_replication_type = "LRS"
  location = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  name = "schaedldstorage"  
}

Wir terraform dann in seiner Reihenfolge mit

terraform init                

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/azurerm from the dependency lock file
- Using previously-installed hashicorp/azurerm v3.0.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -out sampleplan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "switzerlandnorth"
      + name     = "terrfaform-playground"
    }

  # azurerm_storage_account.storage will be created
  + resource "azurerm_storage_account" "storage" {
      + access_tier                       = (known after apply)
      + account_kind                      = "StorageV2"
      + account_replication_type          = "LRS"
      + account_tier                      = "Standard"
      + allow_nested_items_to_be_public   = true
      + enable_https_traffic_only         = true
      + id                                = (known after apply)
      + infrastructure_encryption_enabled = false
      + is_hns_enabled                    = false
      + large_file_share_enabled          = (known after apply)
      + location                          = "switzerlandnorth"
      + min_tls_version                   = "TLS1_2"
      + name                              = "schaedldstorage"
      + nfsv3_enabled                     = false
      + primary_access_key                = (sensitive value)
      + primary_blob_connection_string    = (sensitive value)
      + primary_blob_endpoint             = (known after apply)
      + primary_blob_host                 = (known after apply)
      + primary_connection_string         = (sensitive value)
      + primary_dfs_endpoint              = (known after apply)
      + primary_dfs_host                  = (known after apply)
      + primary_file_endpoint             = (known after apply)
      + primary_file_host                 = (known after apply)
      + primary_location                  = (known after apply)
      + primary_queue_endpoint            = (known after apply)
      + primary_queue_host                = (known after apply)
      + primary_table_endpoint            = (known after apply)
      + primary_table_host                = (known after apply)
      + primary_web_endpoint              = (known after apply)
      + primary_web_host                  = (known after apply)
      + queue_encryption_key_type         = "Service"
      + resource_group_name               = "terrfaform-playground"
      + secondary_access_key              = (sensitive value)
      + secondary_blob_connection_string  = (sensitive value)
      + secondary_blob_endpoint           = (known after apply)
      + secondary_blob_host               = (known after apply)
      + secondary_connection_string       = (sensitive value)
      + secondary_dfs_endpoint            = (known after apply)
      + secondary_dfs_host                = (known after apply)
      + secondary_file_endpoint           = (known after apply)
      + secondary_file_host               = (known after apply)
      + secondary_location                = (known after apply)
      + secondary_queue_endpoint          = (known after apply)
      + secondary_queue_host              = (known after apply)
      + secondary_table_endpoint          = (known after apply)
      + secondary_table_host              = (known after apply)
      + secondary_web_endpoint            = (known after apply)
      + secondary_web_host                = (known after apply)
      + shared_access_key_enabled         = true
      + table_encryption_key_type         = "Service"

      + blob_properties {
          + change_feed_enabled      = (known after apply)
          + default_service_version  = (known after apply)
          + last_access_time_enabled = (known after apply)
          + versioning_enabled       = (known after apply)

          + container_delete_retention_policy {
              + days = (known after apply)
            }

          + cors_rule {
              + allowed_headers    = (known after apply)
              + allowed_methods    = (known after apply)
              + allowed_origins    = (known after apply)
              + exposed_headers    = (known after apply)
              + max_age_in_seconds = (known after apply)
            }

          + delete_retention_policy {
              + days = (known after apply)
            }
        }

      + network_rules {
          + bypass                     = (known after apply)
          + default_action             = (known after apply)
          + ip_rules                   = (known after apply)
          + virtual_network_subnet_ids = (known after apply)

          + private_link_access {
              + endpoint_resource_id = (known after apply)
              + endpoint_tenant_id   = (known after apply)
            }
        }

      + queue_properties {
          + cors_rule {
              + allowed_headers    = (known after apply)
              + allowed_methods    = (known after apply)
              + allowed_origins    = (known after apply)
              + exposed_headers    = (known after apply)
              + max_age_in_seconds = (known after apply)
            }

          + hour_metrics {
              + enabled               = (known after apply)
              + include_apis          = (known after apply)
              + retention_policy_days = (known after apply)
              + version               = (known after apply)
            }

          + logging {
              + delete                = (known after apply)
              + read                  = (known after apply)
              + retention_policy_days = (known after apply)
              + version               = (known after apply)
              + write                 = (known after apply)
            }

          + minute_metrics {
              + enabled               = (known after apply)
              + include_apis          = (known after apply)
              + retention_policy_days = (known after apply)
              + version               = (known after apply)
            }
        }

      + routing {
          + choice                      = (known after apply)
          + publish_internet_endpoints  = (known after apply)
          + publish_microsoft_endpoints = (known after apply)
        }

      + share_properties {
          + cors_rule {
              + allowed_headers    = (known after apply)
              + allowed_methods    = (known after apply)
              + allowed_origins    = (known after apply)
              + exposed_headers    = (known after apply)
              + max_age_in_seconds = (known after apply)
            }

          + retention_policy {
              + days = (known after apply)
            }

          + smb {
              + authentication_types            = (known after apply)
              + channel_encryption_type         = (known after apply)
              + kerberos_ticket_encryption_type = (known after apply)
              + versions                        = (known after apply)
            }
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 

Saved the plan to: sampleplan

To perform exactly these actions, run the following command to apply:
    terraform apply "sampleplan"
terraform apply simpleplan    
azurerm_resource_group.rg: Creating...
azurerm_resource_group.rg: Creation complete after 1s [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_storage_account.storage: Creating...
azurerm_storage_account.storage: Still creating... [10s elapsed]
azurerm_storage_account.storage: Still creating... [20s elapsed]
azurerm_storage_account.storage: Creation complete after 21s [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Nach der Kontrolle im Azure Portal sieht man das Ergebnis.

Azure: Ressource Gruppe.

Die Ressource Gruppe ist erstellt und wenn man diese auswählt, sieht man den darin erstellten StorageAccount.

Azure: Storageaccount.

as Abräumen der Ressource kann dann wie folgt geschehen:

terraform destroy
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_storage_account.storage: Refreshing state... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  - destroy

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be destroyed
  - resource "azurerm_resource_group" "rg" {
      - id       = "/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground" -> null
      - location = "switzerlandnorth" -> null
      - name     = "terrfaform-playground" -> null
      - tags     = {} -> null
    }

  # azurerm_storage_account.storage will be destroyed
  - resource "azurerm_storage_account" "storage" {
      - access_tier                       = "Hot" -> null
      - account_kind                      = "StorageV2" -> null
      - account_replication_type          = "LRS" -> null
      - account_tier                      = "Standard" -> null
      - allow_nested_items_to_be_public   = true -> null
      - enable_https_traffic_only         = true -> null
      - id                                = "/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage" -> null
      - infrastructure_encryption_enabled = false -> null
      - is_hns_enabled                    = false -> null
      - location                          = "switzerlandnorth" -> null
      - min_tls_version                   = "TLS1_2" -> null
      - name                              = "schaedldstorage" -> null
      - nfsv3_enabled                     = false -> null
      - primary_access_key                = (sensitive value)
      - primary_blob_connection_string    = (sensitive value)
      - primary_blob_endpoint             = "https://schaedldstorage.blob.core.windows.net/" -> null
      - primary_blob_host                 = "schaedldstorage.blob.core.windows.net" -> null
      - primary_connection_string         = (sensitive value)
      - primary_dfs_endpoint              = "https://schaedldstorage.dfs.core.windows.net/" -> null
      - primary_dfs_host                  = "schaedldstorage.dfs.core.windows.net" -> null
      - primary_file_endpoint             = "https://schaedldstorage.file.core.windows.net/" -> null
      - primary_file_host                 = "schaedldstorage.file.core.windows.net" -> null
      - primary_location                  = "switzerlandnorth" -> null
      - primary_queue_endpoint            = "https://schaedldstorage.queue.core.windows.net/" -> null
      - primary_queue_host                = "schaedldstorage.queue.core.windows.net" -> null
      - primary_table_endpoint            = "https://schaedldstorage.table.core.windows.net/" -> null
      - primary_table_host                = "schaedldstorage.table.core.windows.net" -> null
      - primary_web_endpoint              = "https://schaedldstorage.z1.web.core.windows.net/" -> null
      - primary_web_host                  = "schaedldstorage.z1.web.core.windows.net" -> null
      - queue_encryption_key_type         = "Service" -> null
      - resource_group_name               = "terrfaform-playground" -> null
      - secondary_access_key              = (sensitive value)
      - secondary_connection_string       = (sensitive value)
      - shared_access_key_enabled         = true -> null
      - table_encryption_key_type         = "Service" -> null
      - tags                              = {} -> null

      - blob_properties {
          - change_feed_enabled      = false -> null
          - last_access_time_enabled = false -> null
          - versioning_enabled       = false -> null
        }

      - network_rules {
          - bypass                     = [
              - "AzureServices",
            ] -> null
          - default_action             = "Allow" -> null
          - ip_rules                   = [] -> null
          - virtual_network_subnet_ids = [] -> null
        }

      - queue_properties {

          - hour_metrics {
              - enabled               = true -> null
              - include_apis          = true -> null
              - retention_policy_days = 7 -> null
              - version               = "1.0" -> null
            }

          - logging {
              - delete                = false -> null
              - read                  = false -> null
              - retention_policy_days = 0 -> null
              - version               = "1.0" -> null
              - write                 = false -> null
            }

          - minute_metrics {
              - enabled               = false -> null
              - include_apis          = false -> null
              - retention_policy_days = 0 -> null
              - version               = "1.0" -> null
            }
        }

      - share_properties {

          - retention_policy {
              - days = 7 -> null
            }
        }
    }

Plan: 0 to add, 0 to change, 2 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

azurerm_storage_account.storage: Destroying... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground/providers/Microsoft.Storage/storageAccounts/schaedldstorage]
azurerm_storage_account.storage: Destruction complete after 2s
azurerm_resource_group.rg: Destroying... [id=/subscriptions/YOURSUBSCRIPTION/resourceGroups/terrfaform-playground]
azurerm_resource_group.rg: Still destroying... [id=/subscriptions/YOURSUBSCRIPTION-...4/resourceGroups/terrfaform-playground, 10s elapsed]
azurerm_resource_group.rg: Destruction complete after 16s

Destroy complete! Resources: 2 destroyed.

Nach dessen Ausführung ist dann auch im Azure-Portal nichts mehr zu sehen.

„Azure: Ressourcegruppe entfernt.

Fazit

Ein einfacher Weg Infrastruktur auch in Azure zu erstellen, ohne die dauernde Anmeldung und der Möglichkeit, Terraform automatisiert in einer CI/CD Pipeline laufen zu lassen.

Daniel Schädler: Quickstart: Bereitstellung einer statischen Webseite auf AWS

In diesem Artikel möchte ich die Schritte für das Veröffentlichen einer statischen Webseite, zum Beispiel einer "Landing Page" mit Terraform und AWS zeigen.

Voraussetzung

  • Ein AWS Konto ist eingerichtet.
  • Terraform ist installiert und konfiguriert für den Zugriff auf AWS.

Vorgehen

Folgende Schritte werden in der Terraform ausgeführt, damit eine statische Webseite auf AWS veröffentlicht werden kann.

Bucket erstellen

Im ersten Schritt wird ein Bucket erstellt.

    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 3.0"
        }
      }
    }
    
    # Configure the AWS Provider
    provider "aws" {
      region = "eu-central-1"
      access_key = "DEIN SCHLÜSSEL"
      secret_key = "DEIN SCHLÜSSEL"
    }
    
    resource "aws_s3_bucket" "webapp" {
      bucket = "schaedld-webapp"
      object_lock_enabled = false   
    }

Die Befehle terraform init, terraform plan -out sampleplan, terraform apply sampleplan und terraform destroy (In Produktion eher vorsichtig damit umgehen) ausgeführt. Diese sind durchgängig durch das ganze Beispiel immer wieder anzuwenden.

Terraform init


terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 3.0"...
- Installing hashicorp/aws v3.75.1...
- Installed hashicorp/aws v3.75.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

Terraform plan

    terraform plan -out sampleplan
    
    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # aws_s3_bucket.webapp will be created
      + resource "aws_s3_bucket" "webapp" {
          + acceleration_status         = (known after apply)
          + acl                         = "private"
          + arn                         = (known after apply)
          + bucket                      = "schaedld-webapp"
          + bucket_domain_name          = (known after apply)
          + bucket_regional_domain_name = (known after apply)
          + force_destroy               = false
          + hosted_zone_id              = (known after apply)
          + id                          = (known after apply)
          + object_lock_enabled         = false
          + region                      = (known after apply)
          + request_payer               = (known after apply)
          + tags_all                    = (known after apply)
          + website_domain              = (known after apply)
          + website_endpoint            = (known after apply)
    
          + object_lock_configuration {
              + object_lock_enabled = (known after apply)
    
              + rule {
                  + default_retention {
                      + days  = (known after apply)
                      + mode  = (known after apply)
                      + years = (known after apply)
                    }
                }
            }
    
          + versioning {
              + enabled    = (known after apply)
              + mfa_delete = (known after apply)
            }
        }
    
    Plan: 1 to add, 0 to change, 0 to destroy.
    
    ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 
    
    Saved the plan to: sampleplan
    
    To perform exactly these actions, run the following command to apply:
        terraform apply "sampleplan"

Terraform apply


    terraform apply sampleplan    
    aws_s3_bucket.webapp: Creating...
    aws_s3_bucket.webapp: Creation complete after 2s [id=schaedld-webapp]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

In AWS ist das Bucket erstellt worden.

AWS: Bucket erstellen erfolgreich.

Nun können die weiteren Elemente hinzugefügt werden.

Erstellen der Webseiten Konfiguration

Damit das Bucket auch als Webseite für die Auslieferung von statischem Inhalt funktioniert, muss eine Webseitenkonfigurationselement in Terraform hinzugefügt werden. (Aus Platzgründen habe ich die vorherigen Schritte weggelassen.)

resource "aws_s3_bucket_website_configuration" "webappcfg" {
  bucket = aws_s3_bucket.webapp.bucket
  
  index_document {
    suffix = "index.html"    
  }  
}

Wird nun terraform apply sampleplan ausgeführt, so ist zu sehen, dass 2 Ressourcen erstellt worden sind.

terraform apply sampleplan
aws_s3_bucket.webapp: Creating...
aws_s3_bucket.webapp: Creation complete after 1s [id=schaedld-webapp]
aws_s3_bucket_website_configuration.webappcfg: Creating...
aws_s3_bucket_website_configuration.webappcfg: Creation complete after 0s [id=schaedld-webapp]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Klickt man im Portal auf das Bucket Objekt, gelangt man in die Verwaltungsseite des Objektes.

AWS: Bucket  Objekt

Wählt man die Option Eigenschaften, so gelangt man in die Einstellungen des Buckets. Navigiert man ans untere Ende der Seite, ist folgender Punkt zu sehen:

Hosten einer statischen Webseite.

Hier zu sehen, ist dass diese Option aktiviert ist. Wenn man nun den bereitgestellten Link anklickt, gelangt man auf eine Seite, die einem den Zugriff verwehrt, da noch kein Objekt für einen öffentlichen Lesezugriff vorhanden ist.

AWS: Access Denied.

Nun kann mit dem nächsten Schritt, dem erstellen eines Objekts für das Bucket fortgefahren werden.

Nun kann mit dem nächsten Schritt, dem erstellen eines Objekts für das Bucket fortgefahren werden.

Erstellen eines Objektes im Bucket

Als letztes Puzzle-Teilchen, ist das Objekt für das Bucket hinzuzufügen. In diesem Beispiel ist es ein einfaches Index.html, dass als "Landing Page" verwendet werden könnte, wenn man frischer Besitzer einer Domain ist und gerade die Webseite aufbaut.

resource "aws_s3_bucket_object" "index" {
  bucket = aws_s3_bucket.webapp.bucket
  content_type = "text/html"
  key = "index.html"
  content = <<EOF
            
            
            <a href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js">https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js</a>
            
              
                <div class="container py-4">
                    <div class="p-5 mb-4 bg-light rounded-3">
                      <div class="container-fluid py-5">
                        <h1 class="display-5 fw-bold">Custom jumbotron</h1>
                        <p class="col-md-8 fs-4">Using a series of utilities, you can create this jumbotron, just like the one in previous versions of Bootstrap. Check out the examples below for how you can remix and restyle it to your liking.</p>
                        Example button
                      </div>
                    </div>
                    </div>
                    <footer class="pt-3 mt-4 text-muted border-top">
                      © 2021
                    </footer>
                  </div>
                
              
  EOF
  acl = "public-read"
}

In diesem Beispiel ist eine Webseite als Multiline Content von Terraform HEREDOC Strings drin, die erstellt wird.

Die Ausführung mit terraform apply zeigt dass eine dritte Ressource erstellt worden ist.

    terraform apply sampleplan
    aws_s3_bucket.webapp: Creating...
    aws_s3_bucket.webapp: Creation complete after 2s [id=schaedld-webapp]
    aws_s3_bucket_website_configuration.webappcfg: Creating...
    aws_s3_bucket_object.index: Creating...
    aws_s3_bucket_object.index: Creation complete after 0s [id=index.html]
    aws_s3_bucket_website_configuration.webappcfg: Creation complete after 0s [id=schaedld-webapp]
    
    Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Die Kontrolle im AWS Portal, des Buckets offenbar, dass diese Datei angelegt worden ist.

AWS: Bucket Objekt erstellt.

Wir der Link, in den Eigenschaften des Buckets unter "Hosten einer statischen Webseite" angeklickt so ist nicht mehr die Access Denied Meldung zu sehen, sondern die bereitgestellte Webseite.

AWS: Landing Page erstellt.

Die komplette Terraform Konfiguration sieht dann wie folgt aus:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "eu-central-1"
  access_key = "AKIAXEP7TRWQSFUAJI65"
  secret_key = "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"
}

resource "aws_s3_bucket" "webapp" {
  bucket = "schaedld-webapp"
  object_lock_enabled = false   
}


resource "aws_s3_bucket_website_configuration" "webappcfg" {
  bucket = aws_s3_bucket.webapp.bucket
  
  index_document {
    suffix = "index.html"    
  }  
}

resource "aws_s3_bucket_object" "index" {
  bucket = aws_s3_bucket.webapp.bucket
  content_type = "text/html"
  key = "index.html"
  content = &lt;&lt;EOT
            
            
            <a href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js">https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js</a>
            
              
                <div class="container py-4">
                    <div class="p-5 mb-4 bg-light rounded-3">
                      <div class="container-fluid py-5">
                        <h1 class="display-5 fw-bold">Willkommen</h1>
                        <p class="col-md-8 fs-4">Willkommen auf der Landing Page der Firma Software Sorglos.</p>                        
                      </div>
                    </div>
                    </div>
                    <footer class="pt-3 mt-4 text-muted border-top">
                      © 2021
                    </footer>
                  </div>
                
              
  EOT
  acl = "public-read"
}

Fazit

Mir nur wenig Aufwand, kann ein erster Kontaktpunkt zu einer neuen Firma auf AWS bereitgestellt werden. Dies ist nur ein Beispiel und hat noch keine Sicherheitsfunktionen aktiviert (vgl. Hosting a static Webseite using Amazon S3).

Holger Schwichtenberg: Magdeburger Developer Days vom 16. bis 18.5.2022

Eintrittskarten für die Community-Veranstaltung für Entwickler gibt es bereits ab 40 Euro.

Sebastian Seidel: Creating a .NET MAUI Maps Control

I am currently working on porting a Xamarin Forms app to DOTNET MAUI. The app also uses maps from Apple or Google Maps to display locations. Even though there is no official support in MAUI yet, I want to show you a way to display maps via custom handler.

Daniel Schädler: Quickstart mit AWS und Terraform

In diesem Artikel gehe ich darauf ein, wie man sich vorbereitet, um mit Terraform und AWS arbeiten zu können.

Was braucht es dazu?

Folgende Voraussetzungen müssen erfüllt sein:

  1. Erstellen eines Benutzers im AWS Account, der dann über die API zugreifen kann.
  2. VS Code AWS Extensions runterladen und konfigurieren für den Zugriff auf AWS.
  3. Zugriffsschlüssel erstellen und als CSV herunterladen.
  4. Verifizierung des Zugriffes über die Extension aus Visual Studio Code.
  5. Terraform muss installiert sein.

Erstellen eines IAM Benutzers in AWS

Um automatisiert Ressourcen auf AWS erstellen zu können, muss vorgängig ein sogenannter IAM-Benutzer erstellt werden. Hier kann wie folgt vorgegangen werden.

  1. Im AWS Konto auf die Identity und Accessmanagement navigieren. Man sollte nun schon auf der richtigen Maske landen.
IAM Benutzer in AWS erstellen.
  1. Mit dem drücken des Knopfes "neuer Benutzer" gelangt man die nachfolgende Ansicht für die Parametrisierung des Benutzers.
Neuer Benutzer hinzufügen.

Wichtig hierbei ist, dass die der Haken bei den CLI Tools gesetzt wird, damit man später mit Terraform darauf zugreifen kann.

  1. Nun können die notwendigen Berechtigungen hinzugefügt werden.
Berechtigungen hinzufügen.

  1. Wenn der Benutzer erstellt worden ist, kopiert euch den Access Key und den private Key oder ladet diesen als CSV herunter, damit diese in den nächsten Schritten weiter verwendet werden können.

AWS Extension Visual Studio Code

Als erstes muss im Marketplace von Visual Studio Code nach der Extension für AWS gesucht werden um diese dann installieren zu können.

AWS Extension hinzufügen.

Nun sind die Erweiterungen für AWS installiert. Diese müssen nun konfiguriert werden. Dies kann mit Hilfe der AWS-Erweiterung durchgeführt werden die durch den SetUp Prozess führt. Hierbei ist es wichtig, dass der Access Key und der private Schlüssel notiert worden sind.

Um zu testen ob eine Verbindung mit den Tools auf AWS gemacht werden kann, reicht es nach deren Konfiguration einfach in der Menüleiste eine Ressource zu erstellen um zu sehen ob die Verbindung geklappt hat.

Ich habe mit einem S3 Bucket getestet und bin wie folgt vorgegangen.

  1. Bucket Option in den Erweiterungen auswählen.
AWS Extension Bucket erstellen.
  1. Anschliessend muss nur noch ein Name eingegeben werden, der eineindeutig sein muss.
AWS Extensions Bucket Name eingeben.

  1. Hat man Erfolg und einen eineindeutigen Namen erwischt so kann ein Bucket erstellt werden und man erhält eine Erfolgsmeldung.
AWS Extension Bucket erfolgreich erstellt.

Nun sind alle Schritte gemacht und die Verbindung zu AWS funktioniert.

Terraform SetUp

Damit Terraform funktioniert, müssen die zuvor heruntergeladenen Schlüssel eingetragen werden. Diese Konfiguration sieht dann wie folgt aus:

provider "aws" {
  region = "eu-central-1"
  access_key = "DEINACCESSKEY"
  secret_key = "DEINSECRETKEY"
}

Damit auch hier überprüft werden kann ob die Verbindung mit AWS funktioniert kann auch ein Bucket erstellt werden mit den Terraform Ressourcen.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "eu-central-1"
  access_key = "DEINACCESSKEY"
  secret_key = "DEINSECRETKEY"
}

resource "aws_s3_bucket" "samplebucket" {
  bucket = "schaedlds-sample-bucket"
  object_lock_enabled = false   
}

Wir terraform dann in seiner Reihenfolge mit

terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~&gt; 3.0"...
- Installing hashicorp/aws v3.75.1...
- Installed hashicorp/aws v3.75.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
terraform plan -out sampleplan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated    
with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.samplebucket will be created
  + resource "aws_s3_bucket" "samplebucket" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "schaedlds-sample-bucket"
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = false
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + object_lock_configuration {
          + object_lock_enabled = (known after apply)

          + rule {
              + default_retention {
                  + days  = (known after apply)
                  + mode  = (known after apply)
                  + years = (known after apply)
                }
            }
        }

      + versioning {
          + enabled    = (known after apply)
          + mfa_delete = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────── 

Saved the plan to: simpleplan
terraform apply sampleplan

aws_s3_bucket.samplebucket: Creating...
aws_s3_bucket.samplebucket: Creation complete after 2s [id=schaedlds-sample-bucket]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Nach der Kontrolle im AWS Dashboard sieht man das Ergebnis.

AWS Portal Bucket erfolgreich erstellt mit Terraform.

Das Abräumen der Ressource kann dann wie folgt geschehen:

terraform destroy 

aws_s3_bucket.samplebucket: Refreshing state... [id=schaedlds-sample-bucket]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated    
with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_s3_bucket.samplebucket will be destroyed
  - resource "aws_s3_bucket" "samplebucket" {
      - acl                         = "private" -&gt; null
      - arn                         = "arn:aws:s3:::schaedlds-sample-bucket" -&gt; null
      - bucket                      = "schaedlds-sample-bucket" -&gt; null
      - bucket_domain_name          = "schaedlds-sample-bucket.s3.amazonaws.com" -&gt; null
      - bucket_regional_domain_name = "schaedlds-sample-bucket.s3.eu-central-1.amazonaws.com" -&gt; null
      - force_destroy               = false -&gt; null
      - hosted_zone_id              = "Z21DNDUVLTQW6Q" -&gt; null
      - id                          = "schaedlds-sample-bucket" -&gt; null
      - object_lock_enabled         = false -&gt; null
      - region                      = "eu-central-1" -&gt; null
      - request_payer               = "BucketOwner" -&gt; null
      - tags                        = {} -&gt; null
      - tags_all                    = {} -&gt; null

      - versioning {
          - enabled    = false -&gt; null
          - mfa_delete = false -&gt; null
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_s3_bucket.samplebucket: Destroying... [id=schaedlds-sample-bucket]
aws_s3_bucket.samplebucket: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

Nach dessen Ausführung ist dann auch im AWS-Portal nichts mehr zu sehen.

AWS Portal Ressource mit Terraform abgeräumt.

Fazit

Ein einfacher Weg Infrastruktur auch in AWS zu erstellen und zu löschen aber mit anderen Konzepten als in Azure.

Martin Richter: Der ständige Helfer im (Datei-)Alltag, der SpeedCommander

Seit mehr als 10 Jahren ist der SpeedCommander nun mein täglicher Begleiter auf meinen privaten und auf meinem Firmen-PC. Irgendwie dachte ich mir, dass dies auch eine Erwähnung wert ist, auch wenn ich nur noch wenig blogge.

Ich will einfach mal die Features erwähnen, die ich wirklich jeden Tag nutze.

  • Eingebauter Packer/Entpacker in x-Formaten (darunter auch so manches esoterisches, aber eben nützliches).
  • Selbstentpackende Dateien werden (wenn gewünscht) als Archiv geöffnet.
  • Einfacher Dateifilter um nur bestimmte Dateien anzuzeigen.
  • Komplexes umbenennen von Dateien mit Dateimustern, Regex Filtern und Erstetzungsfunktionen.
  • Zweigansicht (Ansicht aller Dateien ink. Dateien in den Unterordnern)
  • Simple Vergleichsfunktionen zwischen zwei Ordnern
  • Komplexe Synchronisierungsfunktionen zwischen Ordnern
  • Schnellansicht für extrem viele Dateiformate (für mich für EXE/DLL Dateien immer wieder wichtig)
  • FTP/SFTP Client (ich nutze Filezilla nur in Ausnahmefällen)
  • Sehr guter eingebauter Editor.
  • Direkter Zugriff auf Cloudspeicher (Dropbox/Onedrive etc.)

Das ist vermutlich nicht mal gerade mal die Spitze des Eisberges bei all den Funktionen vom SpeedCommander. Aber es sind die Funktionen, die ich nicht mehr missen möchte bei meiner ganz alltäglichen Arbeit.


Copyright © 2017 Martin Richter
Dieser Feed ist nur für den persönlichen, nicht gewerblichen Gebrauch bestimmt. Eine Verwendung dieses Feeds bzw. der hier veröffentlichten Beiträge auf anderen Webseiten bedarf der ausdrücklichen Genehmigung des Autors.
(Digital Fingerprint: bdafe67664ea5aacaab71f8c0a581adf)

Holger Schwichtenberg: Ein erster Blick auf die Ahead-of-Time-Kompilierung in .NET 7.0

.NET 7 bringt den seit langem geplante AOT-Compiler. Ein Vergleich mit dem JIT-Compiler gibt einen ersten Eindruck vom Speicherbedarf und den Einschränkungen.

Holger Schwichtenberg: Supportende für .NET Framework 4.5.2, 4.6 und 4.6.1 sowie .NET 5.0

Einige .NET-Entwickler, die nicht die neusten Versionen nutzen, müssen in Kürze Ihre Software aktualisieren.

Daniel Schädler: Eine Kurzgeschichte über Pfade

Eine Kurzgeschichte über Pfade

In diesem Kurzbeitrag erläutere ich euch wie man sicher mit Pfaden in der Cross-Plattform Entwicklung umgeht.

Voraussetzungen

Folgende Voraussetzungen sind gegeben.

Eine appsettings.json Datei die wie folgt aussieht:

    "PlantUmlSettings": {
      "PlantUmlTheme": "plain",
      "PlantUmlArgs": "-jar plantuml.jar {0}\*{1}*{2} -o {3} -{4}",
      "PlantUmlEndTag": "@enduml",
      "PlantUmlExe": "plantUml\bin\java.exe",
      "PlantUmlFileSuffix": ".plantuml",
      "PlantUmlStartTag": "@startuml",
      "PlantUmlThemeTag": "!theme",
      "PlantUmlWorkDir": "plantUml\bin"
    }

Eine .NET Anwendung, die sowohl auf einem Windows, wie auch interaktiv im GitLab Runner gestartet werden kann.

Leider hatte ich dann immer folgende Fehlermeldung, als die Applikation im GitLab Runner gestartet worden ist:

/src/Sample.Cli\plantUml//bin//java.exe' with working directory '/builds/Sample/src/Sample.Cli\plantUml//bin'. No such file or directory

Detailbetrachtung

Um dem Ganzen ein wenig weiter auf die Spur zu gehen, habe ich mir eine Beispiel-Applikation geschrieben um das Verhalten auf beiden System zu betrachten.

        static void Main(string[] args)
        {
            var environmentVariable = Environment.ExpandEnvironmentVariables("tmp");
            var tempPath = Path.GetTempPath();

            Console.WriteLine($"Value for Environment Variable {environmentVariable}");
            Console.WriteLine($"Value for {nameof(Path.GetTempPath)} {tempPath}");

            Console.ReadKey();
        }

Lässt man dann das Ganze auf einem Windows System mit dotnet wie folgt laufen, sieht das Ergebnis dann so aus:

PS C:\Users\schae> dotnet run --project D:\_Development_Projects\Repos\ConsoleApp1\ConsoleApp1\ConsoleApp1.csproj
Value for Environment Variable tmp
Value for GetTempPath C:\Users\schae\AppData\Local\Temp\

Das Ergebnis ist wie gewünscht. Nun schauen wir uns das auf der WSL2 an.

root@GAMER-001:~/.dotnet# ./dotnet run --project /mnt/d/_Development_Projects/Repos/ConsoleApp1/ConsoleApp1/ConsoleApp1.
csproj
Value for Environment Variable tmp
Value for GetTempPath /tmp/

Schauen wir doch nun ob der Pfad auch existiert:

root@GAMER-001:~/.dotnet# cd /tmp/
root@GAMER-001:/tmp#

Und das ist der erhoffte Pfad.

Die Lösung

Nach ein wenig Recherchieren in der Dokumentation von Microsoft bin ich auf diesen Artikel DirectorySeperatorChar gestossen. Nicht dass er mir mit dieser Methode geholfen hätte, sondern vielmehr mit dem Auszug

The following example displays Path field values on Windows and on Unix-based systems. Note that Windows supports either the forward slash (which is returned by the AltDirectorySeparatorChar field) or the backslash (which is returned by the DirectorySeparatorChar field) as path separator characters, while Unix-based systems support only the forward slash

dass Windows auch Forward-Slashes unterstützt. Manchem wird das sicherlich schon bekannt gewesen sein aber ich selber werde wohl meine Arbeit mit Pfaden, auch in Windows in Zukunft nur noch mit Forwar-Slashes machen.

Nun habe ich das natürlich auch getestet und zwar in der powershell core.

PS C:\Temp> cd C:/Users
PS C:\Users>

Interessant ist der Umstand, dass wenn der Pfad bekannt ist, man den Tabulator betätigt, Windows automatisch Backslashes macht.

Nun sind überall wo Pfade verwendet werden, die Backward-Slashes durch Forward-Slashes zu ersetzen. Die appsettings.json sieht dann nun so aus:

    "PlantUmlSettings": {
      "PlantUmlTheme": "plain",
      "PlantUmlArgs": "-jar plantuml.jar {0}/*{1}*{2} -o {3} -{4}",
      "PlantUmlEndTag": "@enduml",
      "PlantUmlExe": "plantUml/bin/java.exe",
      "PlantUmlFileSuffix": ".plantuml",
      "PlantUmlStartTag": "@startuml",
      "PlantUmlThemeTag": "!theme",
      "PlantUmlWorkDir": "plantUml\bin"
    }

Nun sind auch keine Fehlermeldungen vorhanden, dass der Pfad nicht mehr gefunden werden kann.

Ein weiterer Punkt den ich mitnehmen werde, ist der, dass in Zukunft alles klein geschrieben wird. Auch Variablen im Windows.

Fazit

Mit einfachen Mitteln lassen sich unter Umständen Stunden des Debuggens oder der Fehlersuche vermeiden. Ich hoffe Dir hat der Beitrag gefallen.

Daniel Schädler: Verwendung von Certbot und Azure

In diesem Artikel will ich zeigen, wie man den Certbot einsetzt um ein Zertifikat zu erhalten um dieses anschliessend auf Azure zu installieren.

Voraussetzungen

Folgende Voraussetzungen müssen erfüllt sein:

  • Linux Subsystem für Windows muss installiert sein mit Ubuntu.
  • Auf dem Linux Subsystem für Windows muss certbot installiert sein.
  • Eine eigene Domain und eine Webseite müssen existieren.

Vorbereitung der Webseite

Damit die Anfragen für Dateien ohne Endung auf einer ASP.NET Applikation ankommen, muss folgende Einstellung vorgenommen werden:

Das Beispiel, zeigt den WebHost der verwendet wird, für die Konfiguration der statischen Dateien.

app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true, // serve extensionless files

    OnPrepareResponse = staticFileResponseContext =>
    {
        // Cache Header für Optimierung für Page Speed
        const int durationInSeconds = 60 * 60 * 24 * 365;
        staticFileResponseContext.Context.Response.Headers[HeaderNames.CacheControl] =
            "public,max-age=" + durationInSeconds;
    }
});

Durchführung

Die Durchführung lässt sich in folgende Schritte gliedern:

  1. Verbinden auf die Azure Webseite mit den Azure Cli Tools
  2. Vorbereitung des Certbots local
  3. Dateien und Ordner in der Webseite erstellen
  4. Weiterfahren mit Cerbot
  5. Konvertierung des Zertifikates
  6. Installation des Zertifikates auf Azure

Verbindung auf die Azure Webseite herstellen

Die Verbindung zu Azure und der Webseite geschieht wie folgt.

az login --use-device-code

az subscription --set <%subscriptionId%>

az webapp ssh -n <%webseiten-name%> -g <%resourcegruppenname%>

Nach erfolgtem einloggen sieht man folgenden Azure Willkommensbilschirm:

Last login: Wed Apr  6 18:38:26 2022 from 169.254.130.3
  _____                               
  /  _  \ __________ _________   ____  
 /  /_\  \___   /  |  \_  __ \_/ __ \ 
/    |    \/    /|  |  /|  | \/\  ___/ 
\____|__  /_____ \____/ |__|    \___  >
        \/      \/                  \/ 
A P P   S E R V I C E   O N   L I N U X

Documentation: http://aka.ms/webapp-linux
Dotnet quickstart: https://aka.ms/dotnet-qs
ASP .NETCore Version: 6.0.0
Note: Any data outside '/home' is not persisted
root@bcd2c665073e:~/site/wwwroot# ^C
root@bcd2c665073e:~/site/wwwroot# 

Nun muss in folgenden Ordner navigiert werden:

cd /home/wwwroot/wwwroot

Nun weiter mit dem nächsten Schritt.

### Vorbereitung des Certbots local

Sollte der Certbot noch nicht installiert sein, so kann dies mittels folgendem Befehl durchgeführt werden:

```bash
sudo apt-get install certbot

Ist der Certbot installiert, kann dieser gestartet werden, wie nachfolgen beschrieben:

sudo certbot certonly -d www.dnug-bern.ch -d dnug-bern.ch --manual

Der Certbot startet und man sieht folgende Meldungen:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for www.dnug-bern.ch

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o:

Mit Y bestätigen und man erhält die Instruktionen, wie weiter vorzugehen ist.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Create a file containing just this data:

JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8.RN326tu-yly1wbWDsnoT5mbba-NazH6fhba6WeEfA2s

And make it available on your web server at this URL:

http://www.dnug-bern.ch/.well-known/acme-challenge/JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8

Achtung: Hier nicht bestätigen, ansonsten wird die Validierung fehlschlagen

Dateien und Ordner in der Webseite erstellen

Nun können wir in der bereits geöffneten Webseite in Azure in dem Ordner weiterfahren, in welchem wir vorher schon navigiert sind.

Nun wird der Ordner erstellt, den Certbot verlangt. Um dies zu bewerkstelligen muss wie folgt vorgegangen werden:

mkdir .well-known
mkdir acme-challenge

Nun muss die Datei erstellt werden. In unserem Fall soll die Datei so heissen: JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8

Um dies zu erreichen muss zuerst vim gestartet werden. Hier muss dann die folgende Zeile eingefügt werden: JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8.RN326tu-yly1wbWDsnoT5mbba-NazH6fhba6WeEfA2s. Anschliessend ist die Datei und dem folgenden Namen zu speicher (mit :w in vim) JxOqa2wKiSbAq5R_o66Gs5_sEE9xhuDwyPbv6pfEOJ8

Anschliessend kann die Webseite für das Testen einmal mit der URL die der Certbot angegeben hat, aufgerufen werden. Ist dies erfolgreich, so wird die Datei aufgerufen und man sieht die erfasste Zeichenfolge.

Weiterfahren mit Certbot

Da der Certbot noch darauf wartet eine Bestätigung für die Validierung zu erhalten, drücken wir nun im noch geöffneten Dialog die ENTER-Taste. Ist alles in Ordnung, so erhält man eine Erfolgsmeldung, dass die Validierung erfolgreich war.

Konvertierung des Zertifikates

Anschliessend muss die PEM Datei in eine PFX-Datei umgewandelt werden. Dies erfolgt wie nachfolgend beschrieben:

openssl pkcs12 -inkey privkey.pem -in cert.pem -export -out dnug.bern.pfx

Installation des Zertifikates auf Azure

Anschliessend muss nach nach folgender Anleitung das Zertifikat in Azure hochgeladen werden.

Fazit

So kann in einfachen Schritten das Zertifikat mit Certbot, zwar manuel aktualisiert werden und es entstehen keine weiteren Kosten. Der Nachteil ist, dass in kurzen Intervallen die Aktualisierung durchgeführt werden muss. Ich hoffe Dir hat Dieser Blogbeitrag gefallen.

Jürgen Gutsch: ASP.​NET Core on .NET 7.0 - File upload and streams using Minimal API

It seems the Minimal API that got introduced in ASP.NET Core 6.0 will now be finished in 7.0. One feature that was heavily missed in 6.0 was the File Upload, as well as the possibility to read the request body as a stream. Let's have a look how this would look alike.

The Minimal API

Creating endpoints using the Minimal API is great for beginners, or to create small endpoints like for microservice applications, or of your endpoints need to be super fast, without the overhead of binding routes to controllers and actions. However, endpoints created with the Minimal API might be quite useful.

By adding the mentioned features they are even more useful. And many more Minimal PI improvements will come in ASP.NET Core 7.0.

To try this I created a new empty web app using the .NET CLI

dotnet new web -n MinimalApi -o MinimalApi
cd MinimalApi
code .

This will create the new project and opens it in VSCode.

Inside VSCode open the Program.cs that should look like this

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Here we see a simple endpoint that sends a "Hello World!" on a GET request.

Uploading files using IFormFile and IFormFileCollection

To upload files we should map an endpoint that listens to POST

Inside the Program.cs, lets create two endpoints, one that receives a IFormFile and another one that receives a IFormFileCollection

app.MapPost("/upload", async(IFormFile file) =>
{
    string tempfile = CreateTempfilePath();
    using var stream = File.OpenWrite(tempfile);
    await file.CopyToAsync(stream);

    // dom more fancy stuff with the IFormFile
});

app.MapPost("/uploadmany", async (IFormFileCollection myFiles) => 
{
    foreach (var file in files)
    {
        string tempfile = CreateTempfilePath();
        using var stream = File.OpenWrite(tempfile);
        await file.CopyToAsync(stream);

        // dom more fancy stuff with the IFormFile
    }
});

The IFormfile is the regular interface Microsoft.AspNetCore.Http.IFormFile that contains all the useful information about the uploaded file, like FileName, ContentType, FileSize, etc.

The CreateTempfilePath that is used here is a small method I wrote to generate a temp file and a path to it. It also creates the folder in case it doesn't exist:

static string CreateTempfilePath()
{
    var filename = $"{Guid.NewGuid()}.tmp";
    var directoryPath = Path.Combine("temp", "uploads");
    if (!Directory.Exists(directoryPath)) Directory.CreateDirectory(directoryPath);

    return Path.Combine(directoryPath, filename);
}

The creation of a temporary filename like this is needed because the actual filename and extension should be exposed to the filesystem for security reason.

Once the file is saved, you can do whatever you need to do with it.

Important note: Currently the file upload doesn't work in case there is a cookie header in the POST request or in case authentication is enabled. This will be fixed in one of the next preview versions. For now you should delete the cookies before sending the request

iformfile

Read the request body as stream

This is cool, you can now read the body of a request as a stream and do what ever you like to do. To try it out I created another endpoint into the Program.cs:

app.MapPost("v2/stream", async (Stream body) =>
{
    string tempfile = CreateTempfilePath();
    using var stream = File.OpenWrite(tempfile);
    await body.CopyToAsync(stream);
});

I'm going to use this endpoint to to store a binary in the file system. BTW: This stream is readonly and not buffered, that means it can only be read once:

request body as stream

It works the same way by using a PipeReader instead of a Stream:

app.MapPost("v3/stream", async (PipeReader body) =>
{
    string tempfile = CreateTempfilePath();
    using var stream = File.OpenWrite(tempfile);
    await body.CopyToAsync(stream);
});

Conclusion

This features makes the Minimal API much more useful. What do you think? Please drop a comment about your opinion.

This aren't the only new features that will come in ASP.NET Core 7.0, many more will come. I'm really looking forward to the route grouping that is announced in the roadmap.

Holger Schwichtenberg: Neu in .NET 6 [20]: Kompilierte Modelle in Entity Framework Core

In Entity Framework Core 6.0 kann man nun die Mapping-Modelle zur Entwicklungszeit für einen beschleunigen Anwendungsstart vorkompilieren.

Code-Inside Blog: How to use IE proxy settings with HttpClient

Internet Explorer is - mostly - dead, but some weird settings are still around and “attached” to the old world, at least on Windows 10. If your system administrator uses some advanced proxy settings (e.g. a PAC-file), those will be attached to the users IE setting.

If you want to use this with a HttpClient you need to code something like this:

    string target = "https://my-target.local";
    var targetUri = new Uri(target);
    var proxyAddressForThisUri = WebRequest.GetSystemWebProxy().GetProxy(targetUri);
    if (proxyAddressForThisUri == targetUri)
    {
        // no proxy needed in this case
        _httpClient = new HttpClient();
    }
    else
    {
        // proxy needed
        _httpClient = new HttpClient(new HttpClientHandler() { Proxy = new WebProxy(proxyAddressForThisUri) { UseDefaultCredentials = true } });
    }

The GetSystemWebProxy() gives access to the system proxy settings from the current user. Then we can query, what proxy is needed for the target. If the result is the same address as the target, then no proxy is needed. Otherwise, we inject a new WebProxy for this address.

Hope this helps!

Be aware: Creating new HttpClients is (at least in a server environment) not recommended. Try to reuse the same HttpClient instance!

Also note: The proxy setting in Windows 11 are now built into the system settings, but the API still works :)

x

Holger Schwichtenberg: Infotag Online: Softwareentwickler-Update für .NET- und Web-Entwickler am 31. Mai 2022

Der Infotag am 31. Mai 2022 behandelt .NET 7, C# 11, WinUI3, Cross-Plattform mit MAUI und aktuelle Azure-Features für Entwickler sowie Visual Studio 2022.

Stefan Henneken: IEC 61131-3: SOLID – The Single Responsibility Principle

The Single Responsibility Principle (SRP) is one of the more important of the SOLID principles. It is responsible for decomposition of modules and encapsulates the idea that each unit of code should be responsible for just a single, clearly defined role. This ensures that software remains extensible long term and makes it easier to maintain.

To illustrate the Single Responsibility Principle concept, I’m going to use the example from my previous post (IEC 61131-3: SOLID – The Dependency Inversion Principle). That post showed how to use the Dependency Inversion Principle (DIP) to eliminate fixed dependencies.

Initial state

There are three different lamp types, with a corresponding function block for each (FB_LampOnOff, FB_LampSetDirect and FB_LampUpDown). Each lamp type works in a different way and provides appropriate methods for modifying the output value.

A higher-level controller (FB_Controller) provides access to a single application programming interface (API) for addressing the three lamp types. The Dependency Inversion Principle (DIP) is applied to avoid having a fixed dependency between the controller and lamp types. The unitary API is defined by I_Lamp. The I_Lamp interface is implemented by the abstract function block FB_Lamp. FB_Lamp contains identical program code for all three lamp types. Having all lamp types derived from FB_Lamp means that the controller and lamps are decoupled. Instead of creating instances of specific lamp types, the controller manages just a single reference to FB_Lamp.

Implementation analysis

We’re going to use the function block FB_LampUpDown to evaluate the implementation in more detail. At the beginning of this series of articles, this function block contained only three methods for changing the output value: OneStepDown(), OneStepUp() and OnOff().

1st issue: multiple roles

In applying the Dependency Inversion Principle (DIP), we added the methods DimDown(), DimUp(), Off() and On() via the FB_Lamp abstract function block and the I_Lamp interface. These four methods represent an ‚adapter‘ between FB_Controller and the concrete FB_LampUpDown implementation.

The UML diagram below shows the two roles the FB_LampUpDown component currently performs. The methods inherited from FB_Lamp are marked in blue (role as adapter for FB_Controller). The area marked in green indicates the actual role performed by this function block (role as FB_LampUpDown).

(abstract elements are displayed in italics)

At this point, we might consider designating the OneStepDown(), OneStepUp() and OnOff() methods as PRIVATE. We can only do this, however, if FB_LampUpDown has not previously been used in any other context. Otherwise, every extension would need to ensure that the function block retained backwards compatibility.

Optimizing implementation

As was the case in our demonstration of the Dependency Inversion Principle (DIP), the program as it stands is very maintainable. But what happens if we add additional roles? A future development cycle might, for example, need to implement additional adapters. The actual FB_LampUpDown logic would be lost in the adapter implementation.

Creating the adapter

We therefore need a tool to separate the individual roles. Ideally a tool that ensures that the original implementation of FB_LampUpDown remains unchanged. This will also be necessary if, for example, FB_LampUpDown resides in a PLC library and is therefore outside the developer’s control.

Approach 1: inheritance

One possible solution would be to use inheritance. The new adapter function block (FB_LampUpDownAdapter) inherits from FB_LampUpDown. But it would also have to inherit from FB_Lamp. Since multiple inheritance is, however, not permitted, one option would be to have FB_LampUpDownAdapter implement the I_Lamp interface. In this case, the abstract function block FB_Lamp is rendered redundant.

By inheriting from FB_LampUpDown, the adapter also provides external access to methods that are not required for interaction with the controller. With this approach, therefore, FB_LampUpDownAdapter exposes details of the FB_LampUpDown implementation.

Approach 2: adapter pattern

In this case, the adapter contains an internal instance of FB_LampUpDown. The methods involved in adapter function are simply passed internally to FB_LampUpDown. This approach avoids exposing details of FB_LampUpDown externally.

(abstract elements are displayed in italics)

This approach meets our objective of clearly separating the role of the adapter from the lamp logic. The lamp implementation does not need to be modified.

Sample 1 (TwinCAT 3.1.4024) on GitHub

Optimization analysis

Let’s take a closer look at the program after implementing the Single Responsibility Principle (SRP).

(abstract elements are displayed in italics)

Responsibilities are now clearly separated. If we need to extend the program code, it’s easy to work out which function block we need to modify.

If we need to add additional adapters, there is no need to extend the implementation of the existing function blocks for the lamps. We don’t need to worry about these function blocks becoming more and more bloated over the course of multiple development cycles.

Separating independent roles into individual, independent units of code (function blocks) makes the program easier to maintain. But it also means more function blocks, making it harder to grasp the overall picture. Consequently, we should not be looking to increase the number of function blocks unnecessarily. Creating individual function blocks for individual roles is not always desirable.

Since program functionality is always expanding, function blocks should be split up when they start to grow too big. SOLID principles can help you in implementing this. This raises the question, however, of how we judge when a unit of code has grown too big.

Class Responsibility Collaboration (CRC)

Counting the lines of code is not a good approach to evaluating the complexity of a unit of code. Code metrics like this can be useful tools (worthy of an article in itself), but I’d like to present a method for determining complexity based on the requirements of a unit of code.

The use of the term ‚unit of code‘, rather than ‚function block‘, is deliberate. This approach can also be used to evaluate a system architecture. In this case, the units of code might be, for example, the individual services. This method is not confined to evaluating pure source code alone.

The method I’m going to look at is called Class Responsibility Collaboration (CRC). The name gives a pretty good insight into the principle behind this method.

  • We start by listing all of the function blocks (class).
  • We then write down the role or responsibility of each function block.
  • We then note down which other function blocks each function block collaborates with (collaboration).

The CRC method flags up very clearly any imbalances in a software system. Responsibilities and dependencies should be evenly distributed across all function blocks.

To create CRC cards, I use SimpleCrcTool. This is available on GitHub (https://github.com/guidolx/simple-crc-app) and can be run directly in a browser: https://guidolx.github.io/simple-crc-app.

To keep things simple, our analysis will ignore the function block FB_AnalogValue. In all variants of our sample program, this operates in the same way to transfer the output value between the relevant lamp type and the controller.

Step 1: Initial state

We will start by analysing the program in its original form, i.e. before we undertook any optimisation (see IEC 61131-3: SOLID – The Dependency Inversion Principle).

We can clearly see that the controller performs a very large number of roles, but the functionality of each lamp type is easily understood. It’s a similar story with dependencies. The controller addresses each lamp type directly.

Step 2: Applying the Dependency Inversion Principle (DIP)

By applying the Dependency Inversion Principle, we eliminate fixed dependencies between the controller and lamp types. Now, the controller only addresses the abstract function block FB_Lamp, and no longer addresses each lamp type.

The disadvantage with this setup is that each lamp type performs more than one role – the logic for the specific lamp type and mapping to the abstract lamp.

Step 3: Applying the Single Responsibility Principle (SRP)

To bring this setup in line with the Single Responsibility Principle, we use the adapter pattern. Each lamp type now has an adapter function block responsible for mapping between the abstract lamp and the specific lamp type.

After optimisation, each function block performs just a single role. We now have a large number of small, rather than a small number of large function blocks.

The definition of the Single Responsibility Principle

Now let’s take a look at the definition of the Single Responsibility Principle. The principle was defined in the book (Amazon ad link*) Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin back in the early 2000s as:

A class should have only one reason to change.

Robert C. Martin has also expressed this as:

A module should be responsible to one, and only one, actor.

But what does module mean in this context and who or what is an actor?

A module in this context is a unit of code. What a module is depends on the angle from which you’re looking at the software system. From the point of view of a software architect, a module might be a REST service, a communication channel or a database system. For a software developer, a module might be a function block or an interrelated set of function blocks and functions. In the above example, the modules were function blocks.

Similarly, the term actor does not necessarily represent a person; it can also refer to a specific set of users or stakeholders.

Summary

In the previous post, I applied the Dependency Inversion Principle (DIP) to decouple the controller (FB_Controller) from the individual lamps. This also required modifications to the individual lamp function blocks. The Single Responsibility Principle (SRP) was then used to further optimise this decoupling.

Is it good practice if a single function block is responsible for compressing and encrypting data? No! Compression and encryption are completely different responsibilities. You can compress data without worrying about encryption. Similarly, encryption is independent of compression. They are completely independent roles. If compression and encryption were dealt with within the same function block, there would be two reasons to change – encryption and compression.

A further example of the Single Responsibility Principle in action (from a software architecture perspective) is the ISO/OSI model for network protocols. The model defines seven sequential layers, each performing clearly defined roles. This makes it possible to replace individual layers without affecting higher or lower layers. Each layer has a single clearly defined role, e.g. transmission of raw bits.

My next post will look at the Liskov Substitution Principle (LSP).

Holger Schwichtenberg: Vue.js 3 ist jetzt der neue Standard für Vue.js

Bisher fokussierte die Vue.js-Website noch auf Version 2 und an diversen Stellen war von Version 3 nur als "next" die Rede. Seit dem 7.2.2022 hat das Entwicklungsteam nun Version 3.x zum Standard erklärt.

Holger Schwichtenberg: Neu in .NET 6 [19]: Migration Bundles in Entity Framework Core

Ein Migration Bundle ist eine ausführbare Datei, die alle erforderlichen Datenbankschemamigrationen durchführt.

Jürgen Gutsch: ASP.NET Core on .NET 7.0 - Roadmap, preview 1 and file upload in minimal APIs

I really like the transparent development of .NET and ASP.NET Core. It is all openly discussed publicly announced on GitHub and developer blogs.

Same with the the first preview version of .NET 7.0 which is released just a couple of days ago. Three months after .NET 6.0 was released. This is thee chance to have the first glimpse at ASP.NET Core 7.0 which will be released beginning of November this year.

Roadmap for ASP.NET Core 7.0

Did you know that there is already a roadmap for ASP.NET Core 7.0? It actually is and it is full of improvements:

ASP.NET Core Roadmap for .NET 7

Even in version 7.0 Microsoft is planning to improve the runtime performance. Also, the ASP.NET Core web frameworks will be improved. Minimal API, SignalR, and Orleans are the main topics here but also Rate Limiting is a topic. There are also a lot of issues about the web UI technologies Maui, Blazor, MVC and the Razor Compiler are the main topics here.

The roadmap refers to the specific GitHub issues that contain a lot of exciting discussions. I would propose to have a detailed look at some of those

ASP.NET Core 7.0 Preview 1

Just a couple of days ago Microsoft released the first preview version of .NET 7.0 and Daniel Roth published a detailed explanation about what was done in ASP.NET Core with this release.

ASP.NET Core updates in .NET 7 Preview 1

Even this year, I will go through the previews and write about interesting upcoming features that will be in the final release like this one:

IFormFile and IFormFileCollection support in minimal APIs

This is an improvement that is requested since the Minimal API was announced the first time. You can now handle uploaded files in minimal APIs using IFormFile and IFormFileCollection.

app.MapPost("/upload", async(IFormFile file) =>
{
    using var stream = System.IO.File.OpenWrite("upload.txt");
    await file.CopyToAsync(stream); 
});
app.MapPost("/upload-many", async (IFormFileCollection myFiles) => { ... });

(This snippet was copied from the blog post mentioned above.)

I'm sure this makes the minimal APIs more useful than before even if there is some limitation that will be addressed in later preview releases of .NET 7.0.

What's next?

As mentioned, I'll pick interesting features from the roadmap and the announcement posts to have a little deeper look at those features and to write about it.

Stefan Henneken: IEC 61131-3: SOLID – Das Single Responsibility Principle

Das Single Responsibility Principle (SRP) ist eines der wichtigsten unter den SOLID-Prinzipien. Es ist für die Zerlegung von Modulen zuständig und verdeutlicht, warum eine Codeeinheit nur für eine einzig klar definierte Aufgabe verantwortlich sein sollte: Software bleibt langfristig erweiterbar und kann deutlich einfacher gepflegt werden.

Um das Single Responsibility Principle näher zu bringen, werde ich auf das Beispiel vom letzten Post (IEC 61131-3: SOLID – Das Dependency Inversion Principle) aufsetzen. Dort wurde gezeigt, wie es möglich ist mit Hilfe des Dependency Inversion Principle (DIP) feste Abhängigkeiten aufzulösen.

Ausgangssituation

Für drei verschiedene Lampentypen stehen jeweils entsprechende Funktionsblöcke (FB_LampOnOff, FB_LampSetDirect und FB_LampUpDown) zur Verfügung. Jeder Lampentyp besitzt seine eigene Funktionsweise und bietet entsprechende Methoden an, um den Ausgangswert zu verändern.

Ein übergeordneter Controller (FB_Controller) stellt eine einheitliche Schnittstelle (API) zur Verfügung, um auf diese drei Typen zuzugreifen. Hierbei wird das Dependency Inversion Principle (DIP) angewendet, um eine feste Kopplung zwischen dem Controller und den Lampentypen zu vermeiden. Durch I_Lamp wird diese einheitliche API definiert. Der abstrakte Funktionsblock FB_Lamp implementiert die Schnittstelle I_Lamp. Des Weiteren enthält FB_Lamp Programmcode, der bei allen Lampentypen gleich ist. Dadurch das alle Lampentypen von FB_Lamp abgeleitet sind, werden Controller und Lampen voneinander entkoppelt. Statt Instanzen von konkreten Lampentypen anzulegen, verwaltet der Controller nur noch eine Referenz auf FB_Lamp.

Analyse der Implementierung

Für eine weitere Beurteilung der Implementierung soll der Funktionsblock FB_LampUpDown dienen. Ganz zu Beginn der Serie enthielt dieser nur die drei Methoden OneStepDown(), OneStepUp() und OnOff() um den Ausgangswert zu verändern.

Punkt 1: mehrere Rollen

Durch die Anwendung des Dependency Inversion Principle (DIP) sind die Methoden DimDown(), DimUp(), Off() und On() über den abstrakten Funktionsblock FB_Lamp und der Schnittstelle I_Lamp hinzugekommen. Diese vier Methoden stellen eine Art ‚Adapter‘ zwischen FB_Controller und der eigenen Implementierung von FB_LampUpDown dar.

Das folgende UML-Diagramm zeigt nochmal die beiden Rollen, die der Baustein FB_LampUpDown aktuell besitzt. Blau markiert sind die Methoden, die durch die Vererbung von FB_Lamp hinzugekommen sind (Rolle als Adapter zu FB_Controller). Der grün markierte Bereich kennzeichnet die eigentliche Rolle des Funktionsbaustein (Rolle als FB_LampUpDown).

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

An dieser Stelle könnte die Überlegung gemacht werden, die Methoden OneStepDown(), OneStepUp() und OnOff() auf PRIVATE zu setzen. Allerdings ist dieses nur dann möglich, wenn FB_LampUpDown bisher in keinem anderen Zusammenhang verwendet wurde. Ist dieses nicht der Fall, so muss jede Erweiterung die Abwärtskompatibel des Funktionsblocks sicherstellen.

Optimierung der Implementierung

So wie auch bei der Vorstellung des Dependency Inversion Principle (DIP), ist das Programm in seinem aktuellen Umfang sehr gut wartbar. Doch was ist, wenn zusätzliche Rollen hinzukommen? So könnte in einem weiteren Entwicklungszyklus es notwendig sein, weitere Adapter zu implementieren. Die eigentliche Logik von FB_LampUpDown würde in den Implementierungen der jeweiligen Adapter untergehen.

Adapter erstellen

Wir brauchen also ein Werkzeug, um die einzelnen Rollen zu separieren. Im besten Fall so, dass die ursprüngliche Implementierung von FB_LampUpDown unverändert bleibt. Dieses kann auch notwendig sein, z.B. dann, wenn sich FB_LampUpDown in einer SPS-Bibliothek befindet und somit nicht im Einflussbereich des Entwicklers liegt.

Ansatz 1: Vererbung

Ein möglicher Lösungsansatz könnte darin bestehen, mit Vererbung zu arbeiten. Der neue Adapter-Funktionsblock (FB_LampUpDownAdapter) erbt von FB_LampUpDown. Zusätzlich müsste dieser ebenfalls von FB_Lamp erben. Da Mehrfachvererbung aber nicht möglich ist, könnte FB_LampUpDownAdapter aber die Schnittstelle I_Lamp implementieren. Der abstrakte Funktionsblock FB_Lamp würde entfallen.

Durch das Erben von FB_LampUpDown stellt der Adapter aber auch die Methoden nach Außen zur Verfügung, die für die Interaktion mit dem Controller nicht benötigt werden. FB_LampUpDownAdapter gibt somit durch diesen Lösungsansatz Implementierungsdetails von FB_LampUpDown weiter.

Ansatz 2: Adapter Pattern

Hierbei enthält der Adapter intern eine Instanz von FB_LampUpDown. Die Methoden für die Funktion des Adapters werden intern einfach an FB_LampUpDown weitergeleitet. Alle Details von FB_LampUpDown werden somit nicht mehr nach Außen bekanntgegeben.

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Mit diesem Ansatz haben wir unser Ziel erreicht: Die Rolle des Adapters und die Logik der Lampe sind klar voneinander getrennt. Die Implementierung der Lampe musste hierzu nicht verändert werden.

Beispiel 1 (TwinCAT 3.1.4024) auf GitHub

Analyse der Optimierung

Schauen wir uns das Programm nach der Umsetzung des Single Responsibility Principle (SRP) nochmal genauer an.

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Die Zuständigkeiten sind jetzt klar voneinander getrennt. Soll der Programmcode erweitert werden, so ist sehr schnell klar, in welchem Funktionsblock dieses zu erfolgen hat.

Auch wenn die Anwendung um weitere Adapter ergänzt wird, so muss die Implementierung der schon existierenden Funktionsblöcke für die Lampen nicht erweitert werden. Es besteht nicht die Gefahr, dass sich diese im Laufe der einzelnen Entwicklungszyklen immer weiter aufblähen.

Die Wartbarkeit eines Programms verbessert sich, wenn unabhängige Aufgaben (Rollen) in einzelne, unabhängige Codeeinheiten (Funktionsblöcke) aufgeteilt werden. Dadurch erhalten wir aber auch mehr Funktionsblöcke, wodurch die Übersicht des Projektes leidet. Aus diesem Grund sollte nicht versucht werden, die Anzahl der Funktionsblöcke unnötig zu erhöhen. Nicht immer ist es sinnvoll einzelne Funktionsblöcke für einzelne Aufgaben anzulegen.

Da ein Programm in seinem Funktionsumfang kontinuierlich erweitert wird, sollten Funktionsblöcke ab einen bestimmten Umfang aufgeteilt werden. Hilfestellung für die Umsetzung geben die SOLID-Prinzipien. Bleibt aber noch die Frage offen, ab wann eine Codeeinheit eine ‚kritische‘ Größe erreicht hat.

Class Responsibility Collaboration (CRC)

Die Anzahl der Codezeilen heranzuziehen, um die Komplexität der Codeeinheit zu beurteilen, ist deutlich zu kurz gegriffen. Auch wenn solche Code-Metriken sinnvolle Hilfsmittel darstellen (das wäre einen eigenen Post wert), so will ich hier ein Verfahren vorstellen, das über die Anforderungen einer Codeeinheit die Komplexität ermittelt.

Ich habe hier bewusst ‚Codeeinheit‘ geschrieben und nicht ‚Funktionsblock‘. Mit diesem Verfahren kann auch eine Systemarchitektur beurteilt werden. Die ‚Codeeinheiten‘ wären dann z.B. einzelne Services. Es muss also nicht immer um die Beurteilung von reinem Quellcode gehen.

Die hier vorgestellte CRC-Technik steht für Class Responsibility Collaboration. Der Name beschreibt schon recht gut das Prinzip dieser Technik:

  • Es werden alle Funktionsblöcke (Class) aufgelistet.
  • Zu jedem Funktionsblock wird die Aufgabe bzw. Zuständigkeit (Responsibility) aufgeschrieben.
  • Außerdem wird bei jedem Funktionsblock notiert, mit welchen anderen Funktionsblöcken dieser zusammenarbeitet (Collaboration).

Die CRC-Technik zeigt sehr deutlich, ob sich in einem Softwaresystem ein Ungleichgewicht befindet. Die Zuständigkeiten und die Abhängigkeiten sollten sich gleichmäßig über alle Funktionsblöcke verteilen.

Für das Erstellen der CRC-Karten verwende ich das Tool SimpleCrcTool, welches auf GitHub (https://github.com/guidolx/simple-crc-app) zu finden ist und direkt im Browser ausgeführt werden kann: https://guidolx.github.io/simple-crc-app.

Um die Übersicht zu erhöhen, wird bei der folgenden Betrachtung der Funktionsblock FB_AnalogValue nicht weiter berücksichtigt. Dieser dient in allen Varianten des Beispielprogramms in gleicher Weise zum Austausch der Ausgangsgröße zwischen den jeweiligen Lampentypen und dem Controller.

Schritt 1: Ausgangssituation

Zu Beginn soll das Programm in seiner Ausgangsform betrachtet werden. Also bevor die erste Optimierung durchgeführt wurde (siehe IEC 61131-3: SOLID – Das Dependency Inversion Principle).

Es ist gut zu erkennen, dass der Controller sehr viele Aufgaben übernimmt, während der Umfang der jeweiligen Lampentypen sehr übersichtlich ist. Ähnlich sieht es bei den Abhängigkeiten aus. Der Controller greift auf jeden Lampentyp direkt zu.

Schritt 2: Anwendung des Dependency Inversion Principle (DIP)

Durch die Anwendung des Dependency Inversion Principle wurden die festen Abhängigkeiten zwischen dem Controller und den Lampentypen aufgelöst. Der Controller greift nur noch auf den abstrakten Funktionsblock FB_Lamp zu und nicht mehr auf die jeweiligen spezialisierten Lampentypen.

Jetzt besteht allerdings der Nachteil, dass jeder Lampentyp mehrere Rollen bedient. Zum einen die Logik des Lampentyps und zum anderen das Mapping zu der abstrakten Lampe.

Schritt 3: Anwendung des Single Responsibility Principle (SRP)

Um die Verletzung des Single Responsibility Principle an dieser Stelle aufzulösen, wurden das Adapter Pattern angewendet. Jeder Lampentyp besitzt jetzt einen entsprechenden Adapter-Funktionsblock, der für das Mapping zwischen der abstrakten Lampe und dem konkreten Lampentyp zuständig ist.

Alle Funktionsblöcke besitzen nach der Optimierung nur noch eine einzige Aufgabe. Somit haben wir jetzt eine große Menge an kleinen, statt eine kleine Menge an umfangreichen Funktionsblöcken.

Die Definition des Single Responsibility Principle

Werfen wir nun einen Blick auf die Definition des Single Responsibility Principle. Dieses besteht aus einem Grundsatz und wurde ebenfalls in dem Buch (Amazon-Werbelink *) Clean Architecture: Das Praxis-Handbuch für professionelles Softwaredesign von Robert C. Martin schon Anfang der 2000er Jahre definiert:

Es sollte nie mehr als einen Grund geben, eine Klasse zu modifizieren.

Robert C. Martin verfeinert diese Aussage weiter zu:

Ein Modul sollte für einen, und nur einen, Akteur verantwortlich sein.

Doch was ist mit Modul gemeint und wer oder was ist der Akteur?

Das Modul ist hierbei eine Codeeinheit und ist abhängig von der Perspektive, mit der ein Softwaresystem betrachtet wird. Aus der Sicht des Softwarearchitekten kann ein Modul ein REST-Service, ein Kommunikationskanal oder ein Datenbanksystem sein. Für den Softwareentwickler kann ein Modul ein Funktionsblock oder ein zusammenhängender Satz an Funktionsblöcken und Funktionen darstellen. Bei dem oben gezeigten Beispiel, war ein Modul ein Funktionsblock.

Auch der Begriff Akteur bezieht sich nicht zwangsläufig auf eine Person, sondern kann auch wieder ein bestimmter Satz an Usern oder Stakeholdern repräsentieren.

Zusammenfassung

Im letzten Post wurde durch das Dependency Inversion Principle (DIP) der Controller (FB_Controller) von den einzelnen Lampen entkoppelt. Hierzu mussten noch die einzelnen Funktionsblöcke der Lampen angepasst werden. Durch das Single Responsibility Principle (SRP) wurde diese Entkopplung weiter optimiert.

Ist es in Ordnung, wenn ein Funktionsblock für das Komprimieren und für das Verschlüsseln von Daten zuständig ist? Nein! Komprimieren und Verschlüsseln sind völlig verschiedene Verantwortungsbereiche. Man kann Daten komprimieren, ohne dabei Aspekte der Verschlüsselung zu berücksichtigen. Und auch die Verschlüsselung ist unabhängig von der Komprimierung. Es handelt sich um zwei völlig unabhängige Aufgaben. Werden etwa Komprimierung und Verschlüsselung im selben Funktionsblock behandelt, so gibt es auch zwei Gründe für Änderungen: die Verschlüsselung und die Komprimierung.

Ein weiteres Beispiel für die Anwendung des Single Responsibility Principle (aus der Sicht der Softwarearchitektur) ist das ISO/OSI-Referenzmodell für Netzwerkprotokolle. Dieses Modell definiert sieben aufeinanderfolgende Schichten mit jeweils klar definierten Aufgaben. Dieses ermöglicht das Austauschen einzelner Schichten, ohne das darüber oder darunter liegende Schichten davon beeinflusst werden. Jede Schicht hat eine(!) klar definierte Aufgabe, z.B. die Bitübertragung.

Im nächsten Post geht es um das Liskov Substitution Principle (LSP).

Thorsten Hans: How to Bypass Geo-Blocking?

Many people want to access geo-restricted online content. For instance, they try to bypass geo-blocking on Netflix, BBC iPlayer and other streaming sites. Or here’s an even more precise example: let’s say they try to access BBC iPlayer from outside the UK. Well, they will be blocked as they are not within the UK. Do you know what this means? Keep reading and find out. SPOILER: It’s annoying and restricts your freedom of choice.

Technological advancements now allow you to bypass these nasty geo-blocks without even realizing it. The goal of bypassing geo-blocking is to enjoy online content from different parts of the world. The best ways that are widely known to bypass this problem now are to use a VPN or to use a Proxy server. Bypassing geo-blocking is very easy with those two options.  

What Is Geo-blocking?

The act of blocking access to people in geographical locations outside of their service area – that’s what geo-blocking is. The technology (also known as geo-filtering) is used by many services to restrict access to content based on geographic location. Basically, the services detect when a user is not in the location where their subscription was originally obtained and they get denied access to some material.

When you go to watch movies online and find out that some of them are restricted in your country that means you’ve been geo-filtered. This can also happen if you try to place an order online for something only to be told it’s not available where you live or it would cost extra money because of postage costs or currency exchange rates.

Geoblocking Bypass With a VPN

It is ridiculously easy to bypass geo-blocking with a VPN service provider and there will be no limits whatsoever. A VPN is an essential tool if you want to bypass geo-blocking and watch your favorite movies, listen to music or shop online without the hassle of countries’ restrictions getting in the way.

It works by rerouting your Internet connection through a server in another country to bypass geo-blocking restrictions. In other words, it changes your IP address to that of the country where the content is being broadcast from.

How to Bypass Geo-Blocking With a VPN

You will need to subscribe to a VPN service and download the Client so you’ll be able to bypass geo-blocking in just a couple of minutes. After that, all you have to do is select a server from a different location and enjoy the content you want.

So all you need to do is sign up for a carefully chosen reputable VPN service and then it will connect you via an encrypted connection to another server where the restricted content is available. There are many different VPNs available, but be sure to choose one that bypasses geo-blocking before you sign up for any plan.

Geo-Blocking Bypass With a Proxy Server

Using a proxy is not as effective as a VPN as all it does is change the IP address of the browser you are using to access the site, rather than bypassing geo-blocking completely. But it’s still an option.

So proxy bypasses geo-blocking by hiding your real location and making a proxy server think that you’re located somewhere else. That way you can bypass geo-blocking without any special software or VPNs because your computer already has access to proxy servers, which act as middlemen between your computer and internet sites.

How to Bypass Geo-blocking With Proxy?

Let’s explain it in three easy steps:

  • Step 1. Connect to a proxy bypass server. Go to a website that offers you a list of proxies. After doing this, select one from their list and tap on the submit button.
  • Step 2. A new browser window will open with the proxy bypass server’s address as its URL which you can use to bypass geo-blocking on the current web page of your choice.
  • Step 3. If you’re bypassing geo-blocking of YouTube videos, just right-click on the video and select “settings”. The URL box will appear, so you can enter the proxy bypass server’s address into that box then tap on the OK button to start bypassing geo-blocking and watching videos without any restrictions.

FAQ

What is a better option for bypassing geo-blocking – VPN or proxy?

It is advisable to use a VPN rather than a proxy as a VPN bypasses geo-blocking on both mobile devices and computers. Proxy is not as effective as a Virtual Private Network service as all it does is change the IP address of the browser that is being used to access a certain site, rather than bypassing geo-blocking completely.

Is geo-blocking legal?

Yes, it is. But so is the usage of a Virtual Private Network (VPN), at least in most countries. If the government in a certain country is very strict you should first check out its laws and regulations.

What are some good VPNs for bypassing geo-blocking?

You should choose a paid reputable VPN service. Using a free VPN for geo-blocking bypass is not advisable at all. NordVPN, ExpressVPN and Surfshark are great reliable VPNs.

The post How to Bypass Geo-Blocking? appeared first on Xplatform.

Martin Richter: PTZControl goes GitHub

Mich haben doch einige Anfragen erreicht, die um den Sourcecode von PTZControl gebeten haben.

Ich habe mich entschieden den Code auf GitHub zu veröffentlichen. Ich hoffe, dass dies anderen hilft, oder es nützt den Sourcecode anderweitig zu verwenden.

Vielleicht hilft es so, anderen das Streaming der Gottesdienste mit der PTZ 2 Pro oder Rally einfacher zu gestalten.

Hier der entsprechende Link auf mein Repository:
https://github.com/xMRi/PTZControl


Copyright © 2017 Martin Richter
Dieser Feed ist nur für den persönlichen, nicht gewerblichen Gebrauch bestimmt. Eine Verwendung dieses Feeds bzw. der hier veröffentlichten Beiträge auf anderen Webseiten bedarf der ausdrücklichen Genehmigung des Autors.
(Digital Fingerprint: bdafe67664ea5aacaab71f8c0a581adf)

Holger Schwichtenberg: Neu in .NET 6 [18]: Spaltenreihenfolge in Entity Framework Core

Entity Framework Core bietet seit Version 6.0 wieder die Option, die Reihenfolge der Spalten beim Anlegen von Tabellen anzugeben.

Holger Schwichtenberg: In eigener Sache: Neues Buch zu Vue.js 3

Das neue Fachbuch "Vue.js 3 Crashkurs" richtet sich an Einsteiger in Vue.js 3.x und Umsteiger von Vue.js 2.x.

Jürgen Gutsch: 20 years of .NET

.NET turns 20 years old today and it is just kind of ... wow!

.NET 20 Years

Yes, the 20-year celebration was was announced for a couple of weeks now, but I didn't really care until I started to think about it.

I didn't really plan to write about it but the more I think about the last 20 years... you know... And you are completely free to read it. :-D

Because 20 years of .NET also means to me writing software and getting paid for it for more than 20 years, it also means to me spending almost half of that time as a Software Engineer at the YOO. This is amazing, surprising, and a little bit scary as well. I already spent almost half of my life turning coffee into code.

I started coding using ASP written in VBScript, which also uses server-side ActiveX libraries written in VB6. At that time I tackled the Microsoft developer community the first time, by asking questions about how to solve my coding problems.

At the time of the second half of 2001, there already was lot of news about ASP+, NET Fx, and other weird stuff. And I started to play around and created the first ASP-like applications that actually compile and execute awesomely fast. I was impressed by .NET 1.0 and writing the first ASP.NET Code using VB.NET.

I took some time to convince my boss to write the very first project using ASP.NET 1.0 but finally I got the opportunity to rewrite a news module for an existing CMS using the new technology.

At that time a pretty cool blog was the primary source to learn about new things about .NET and ASP.NET. It was Scott Guthrie's blog. The cool thing: That blog is still online. You'll find posts from 2003. Awesome! Scott was the person who invented ASP.NET. Just yesterday, he posted a tweet that shows his notebook that contains the first specks about ASP.NET.

Now, I'm wondering what this book would look like if it would have been written using interactive notebooks :-D

The release of .NET 1.1 wasn't that good: Project types change and a lot more breaking changes happened and we had doubts about using .NET at all. Luckily Microsoft released a patch that fixes the issues we and a lot more developers had at that time.

My personal 20 years of .NET were a journey with some lows but a lot more heights during the whole time. Not all the stuff I worked with was good. I remember some ugly technology that was put on top of ASP.NET Webforms. Do you remember the ASP.NET Web Parts? I guess this was a bad try to get SharePoint-like behavior in every web application. This feature got even worse together with ASP.NET AJAX. Moving HTML and ViewStates around using JavaScript remote scripting (Ajax) isn't really what Ajax is meant for.

Some years before that, I already worked on a project for Siemens that loads XML-based data via client scripting from the server and displays the data on the client. Also, user input got sent to the server that way. The term Ajax wasn't introduced at that time. We created a single-page application years before the SPAs were a thing at all. Maybe this is the reason why ASP.NET AJAX felt completely wrong to us.

It was .NET 3.5 that changes ASP.NET and .NET a lot. ASP.NET MVC was awesome. It felt a lot more like a real web framework. It doesn't use a ViewState to hack around the stateless nature of the web. Using ASP.NET MVC felt more natural as we knew it from the days when we worked with classic ASP. I started to like .NET again. Even LINQ got introduced and made the handling of data a lot more productive.

Also around that time, I started blogging. I wrote my first blog post in 2007. I already was kinda involved in the German-speaking .NET developer community by contributing what I learned to people that started working with the technology. The feedback was amazing and pushes me forward to continue with the community work. Because of my blog, I got asked to write for technical magazines, and because of this, I got asked to talk at conferences, and so on...

I am mainly a more pragmatic developer. I'm using the tools that are best for me and the current project. .NET isn't always the best tool. I also worked with NodeJS and was impressed by how fast it is to get started and to get your job done. I also worked with python for around a year and had the same experience. Since I started my career scripting solutions using classic ASP, it wasn't a big deal to use JavaScript or Python. Actually, I miss the scripting experience in .NET as well as the flexibility with the language. Maybe I should have a more detailed look into F#. On the other hand, being a pragmatic software developer also means, getting your work done, making the customer happy, and getting paid for your work. In some cases, Python helped me to get things done as well as NodeJS did but in most cases it was .NET that helped me to get the customers happy. Well known since 1.0.

Getting things done using a well-known framework means you can start hacking around issues, customizing stuff to remove blocking things. Actually, I did work on a pretty fast ASP.NET Webforms application that doesn't use any Webforms UI technology at all. No Webforms Components, no ViewState. It generates XML that got transformed on the server using XSL-Templates and writes the result directly to the output stream.

My personal 20 journey is like a marriage with heights and lows. And some unexpected happenings change a marriage a lot. .NET Core happened and I fell in love with .NET again. It was completely rewritten, lightweight and customizable. You all know that already. The best thing from my perspective is the lightweight and console first approach. It almost feels like NodeJS and works similar to all the other web tools and frameworks that already exist at that time. .NET wasn't only a framework to build stuff and earn money for that work anymore. With the new .NET, it started to make fun again.

I am a web developer, even if I did some Windows Forms, WPF, Windows Mobile as well as Windows Phone development. I always feel more comfortable working with applications that have an HTML user interface, that make use of CSS and JavaScript. So the new .NET and ASP.NET Core feels even more naturally than ASP.NET MVC ever did. This is absolutely great.

What's coming next? We'll see.

Writing a book was eating my time to write blog posts. I'm now starting to have a look at the next version of .NET and ASP.NET Core and I will write about that.

There will be no .NET 7.0 update for my book since I decided to write a new edition for every LTS version only. The next LTS will be .NET 8.0 which should be released around November 2023. So I'll have enough time to write blog posts.

🎉 Happy 20th Anniversary .NET! 🎉

Holger Schwichtenberg: Neu in .NET 6 [17]: Datenbankschemakommentare beim Reverse Engineering

Entity Framework Core übernimmt als OR-Mapper Kommentartexte aus SQL Server in C#-Quellcode.

Stefan Henneken: IEC 61131-3: SOLID – The Dependency Inversion Principle

Fixed dependencies are one of the main causes of poorly maintainable software. Certainly, not all function blocks can exist completely independently of other function blocks. After all, these interact with each other and are thus interrelated. However, by applying the Dependency Inversion Principle, these dependencies can be minimized. Changes can therefore be implemented more quickly.

With a simple example, I will show how negative couplings can arise between function blocks. Then, I will resolve these dependencies with the help of the Dependency Inversion Principle.

Example

The example contains three function blocks, each of which controls different lamps. While FB_LampOnOff can only switch a lamp on and off, FB_LampSetDirect can set the output value directly to a value from 0 % to 100 %. The third function block (FB_LampUpDown) is only able to relatively dim the lamp by 1 % using the OneStepDown() and OneStepUp() methods. The method OnOff() sets the output value immediately to 100 % or 0 %.

Task definition

These three function blocks are controlled by FB_Controller. An instance of each lamp type is instantiated in FB_Controller. The desired lamp is selected via the property eActiveLamp of type E_LampType.

TYPE E_LampType :
(
  Unknown   := -1,
  SetDirect := 0,
  OnOff     := 1,
  UpDown    := 2
) := Unknown;
END_TYPE

In turn, FB_Controller has appropriate methods for controlling the different lamp types. The DimDown() and DimUp() methods dim the selected lamp by 5 % upwards or 5 % downwards. While the On() and Off() methods switch the lamp on or off directly.

The IEC 61131-3: The Observer Pattern is used to transmit the output variable between the controller and the selected lamp. The controller contains an instance of FB_AnalogValue for this purpose. FB_AnalogValue implements the interface I_Observer with the method Update(), while the three function blocks for the lamps implement the interface I_Subject. Using the Attach() method, each lamp block receives an interface pointer to the I_Observer interface of FB_AnalogValue. If the output value changes in one of the three lamp blocks, the new value is transferred to FB_AnalogValue from the interface I_Observer via the method Update().

Our example so far consists of the following actors:

The UML diagram shows the relationships between the respective elements:

Let’s take a closer look at the program code of the individual function blocks.

FB_LampOnOff / FB_LampUpDown / FB_LampSetDirect

FB_LampSetDirect is used here as an example for the three lamp types. FB_LampSetDirect has a local variable for the current output value and a local variable for the interface pointer to FB_AnalogValue.

FUNCTION_BLOCK PUBLIC FB_LampSetDirect IMPLEMENTS I_Subject
VAR
  nLightLevel    : BYTE(0..100);
  _ipObserver    : I_Observer;
END_VAR

If FB_Controller switches to the lamp of the type FB_LampSetDirect, FB_Controller calls the Attach() method and passes the interface pointer to FB_AnalogValue to FB_LampSetDirect. If the value is valid (not equal to 0), it is saved in the local variable (backing variable) _ipObserver.

Note: Local variables that store the value of a property are also known as backing variables and are indicated by an underscore in the variable name.

METHOD Attach
VAR_INPUT
  ipObserver     : I_Observer;
END_VAR
IF (ipObserver = 0) THEN
  RETURN;
END_IF
_ipObserver := ipObserver;

The Detach() method sets the interface pointer to 0, which means that the Update() method is no longer called (see below).

METHOD Detach
_ipObserver := 0;

The new output value is passed via the SetLightLevel() method and stored in the local variable nLightLevel. In addition, the method Update() is called by the interface pointer _ipObserver. This gives the new output value to the instance of FB_AnalogValue located in FB_Controller.

METHOD PUBLIC SetLightLevel
VAR_INPUT
  nNewLightLevel    : BYTE(0..100);
END_VAR
nLightLevel := nNewLightLevel; 
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(nLightLevel);
END_IF

The Attach() and Detach() methods are identical for all three lamp blocks. There are differences only in the methods that change the initial value.

FB_AnalogValue

FB_AnalogValue contains very little program code, since this function block is only used to store the output variable.

FUNCTION_BLOCK PUBLIC FB_AnalogValue IMPLEMENTS I_Observer
VAR
  _nActualValue   : BYTE(0..100);
END_VAR
 
METHOD Update : BYTE
VAR_INPUT
  nNewValue       : BYTE(0..100);
END_VAR

In addition, FB_AnalogValue has the property nValue, via which the current value is made available externally.

FB_Controller

FB_Controller contains the instances of the three lamp blocks. Furthermore, there is an instance of FB_AnalogValue to receive the current output value of the active lamp. _eActiveLamp stores the current state of the eActiveLamp property.

FUNCTION_BLOCK PUBLIC FB_Controller
VAR
  fbLampOnOff      : FB_LampOnOff();
  fbLampSetDirect  : FB_LampSetDirect();
  fbLampUpDown     : FB_LampUpDown();
  fbActualValue    : FB_AnalogValue();
  _eActiveLamp     : E_LampType;
END_VAR

Switching between the three lamps is done by the setter of the eActiveLamp property.

Off();
 
fbLampOnOff.Detach();
fbLampSetDirect.Detach();
fbLampUpDown.Detach();
 
CASE eActiveLamp OF
  E_LampType.OnOff:
    fbLampOnOff.Attach(fbActualValue);
  E_LampType.SetDirect:
    fbLampSetDirect.Attach(fbActualValue);
  E_LampType.UpDown:
    fbLampUpDown.Attach(fbActualValue);
END_CASE
 
_eActiveLamp := eActiveLamp;

If the eActiveLamp property is used to switch to another lamp, the current lamp is switched off at first using the local method Off(). Furthermore, the method Detach() is called for all three lamps. This terminates a possible connection to FB_AnalogValue. Within the CASE statement, the method Attach() is called for the new lamp and the interface pointer is passed to fbActualValue. Finally, the state of the property is saved in the local variable _eActiveLamp.

The methods DimDown(), DimUp(), Off() and On() have the task of setting the desired output value. Since the individual lamp types offer different methods for this, each lamp type must be handled individually.

The DimDown() method should dim the active lamp by 5 %. However, the initial value should not fall below 10 %.

METHOD PUBLIC DimDown
CASE _eActiveLamp OF
  E_LampType.OnOff:
    fbLampOnOff.Off();
  E_LampType.SetDirect:
    IF (fbActualValue.nValue >= 15) THEN
      fbLampSetDirect.SetLightLevel(fbActualValue.nValue - 5);
    END_IF
  E_LampType.UpDown:
    IF (fbActualValue.nValue >= 15) THEN 
      fbLampUpDown.OneStepDown();
      fbLampUpDown.OneStepDown();
      fbLampUpDown.OneStepDown();
      fbLampUpDown.OneStepDown();
      fbLampUpDown.OneStepDown();
    END_IF
END_CASE

FB_LampOnOff only knows the states 0 % and 100 %. Dimming is therefore not possible. As a compromise, the lamp will in fact be switched off when it is dimmed down (line 4).

With FB_LampSetDirect, the SetLightLevel() method can be used to set the new initial value directly. To do this, 5 is subtracted from the current output value and passed to the SetLightLevel() method (line 7). The IF query in line 6 ensures that the initial value is not set below 10 %.

Since the OneStepDown() method of FB_LampUpDown only reduces the initial value by 1 %, the method is called 5 times (lines 11-15). Here again, the IF query in line 10 ensures that the value does not fall below 10 %.

DimUp(), Off() and On() have a comparable structure. The various lamp types are treated separately using a CASE statement, and the respective special features are thus taken into account.

Sample 1 (TwinCAT 3.1.4024) on GitHub

Implementation analysis

At first glance, the implementation seems solid. The program does what it should and the presented code is maintainable in its current size. If it were ensured that the program would not increase in size, everything could remain as it is.

But in practice, the current state is more like the first development cycle of a larger project. The small manageable application will grow in code size over time as extensions are added. Thus, a close inspection of the code right at the beginning makes sense. Otherwise, there is a risk of missing the right time for fundamental optimizations. Defects can then only be eliminated with a great deal of time.

But what are the fundamental issues with the above example?

1st issue: CASE statement

Every method of the controller has the same CASE construct.

CASE _eActiveLamp OF
  E_LampType.OnOff:
    fbLampOnOff...
  E_LampType.SetDirect:
    fbLampSetDirect...
  E_LampType.UpDown:
    fbLampUpDown...
END_CASE

Although there is a similarity between the value of _eActiveLamp (e.g., E_LampType.SetDirect) and the local variable (e.g., fbLampSetDirect), the individual cases have still to be observed and programmed manually.

2nd issue: Extensibility

If a new lamp type has to be added, the data type E_LampType must first be extended. Then, it is necessary to add the CASE statement in each method of the controller.

3rd issue: Responsibilities

Because the controller assigns the commands to all lamp types, the logic of a lamp type is distributed over several FBs. This is an extremely impractical grouping. If you want to understand how the controller addresses a specific lamp type, you have to jump from method to method and pick the correct case in the CASE statement.

4th issue: Coupling

The controller has a close connection to the different lamp modules. As a result, the controller is highly dependent on changes to the individual lamp types. Every change to the methods of a lamp type inevitably leads to adjustments of the controller.

Optimizing implementation

Currently, the example has fixed dependencies in one direction. The controller calls the methods of the respective lamp types. This direct dependency should be resolved. To do this, we need a common level of abstraction.

Resolving the CASE statements

Abstract function blocks and interfaces can be used for this purpose. In the following, I use the abstract function block FB_Lamp and the interface I_Lamp. The interface I_Lamp has the same methods as the controller. The abstract FB implements the interface I_Lamp and thus also has all the methods of FB_Controller.

I presented in IEC 61131-3: Abstract FB vs. interface, how abstract function blocks and interfaces can be combined with each other.

All lamp types inherit from this abstract lamp type. This makes all lamp types look the same from the controller’s point of view. Furthermore, the abstract FB implements the I_Subject interface.

FUNCTION_BLOCK PUBLIC ABSTRACT FB_Lamp IMPLEMENTS I_Subject, I_Lamp

The methods Detach() and Attach() of FB_Lamp are not declared as abstract and contain the necessary program code. This means that it is not necessary to implement the program code for these two methods in each lamp type again.

Since the lamp types inherit from FB_Lamp, they are all the same from the controller’s point of view.

The SetLightLevel() method remains unchanged. The assignment of the methods of FB_Lamp (DimDown(), DimUp(), Off() and On()) to the respective lamp types is now no longer done in the controller, but in the respective FB of the lamp type:

METHOD PUBLIC DimDown
IF (nLightLevel >= 15) THEN
  SetLightLevel(nLightLevel - 5);
END_IF

Thus, the controller is no longer responsible for assigning the methods, but rather each lamp type itself. The CASE statements in the FB_Controller methods are omitted completely.

Resolving E_LampType

The use of E_LampType still binds the controller to the respective lamp types. But how to switch to the different lamp types if E_LampType is omitted? To achieve this, the desired lamp type is passed to the controller via a property by reference.

PROPERTY PUBLIC refActiveLamp : REFERENCE TO FB_Lamp

Thus, all lamp types can be passed. The only condition is that the passed lamp type must inherit from FB_Lamp. This defines all methods and properties that are necessary for an interaction between the controller and the lamp block.

Note: This technique of ‚injecting‘ dependencies is also called Dependency Injection.

Switching to the new lamp module is done in the setter of the refActiveLamp property. The method Detach() of the active lamp is called there (line 2), while the method Attach() is called in line 6 by the new lamp. In line 4, the reference of the new lamp is stored in the local variable (backing variable) _refActiveLamp.

IF (__ISVALIDREF(_refActiveLamp)) THEN
  _refActiveLamp.Detach();
END_IF
_refActiveLamp REF= refActiveLamp;
IF (__ISVALIDREF(refActiveLamp)) THEN
   refActiveLamp.Attach(fbActualValue);
END_IF

In the methods DimDown(), DimUp(), Off() and On(), the method call is forwarded to the active lamp via _refActiveLamp. Instead of the CASE statement, there are only a few lines here, since it is no longer necessary to distinguish between the different lamp types.

METHOD PUBLIC DimDown
IF (__ISVALIDREF(_refActiveLamp)) THEN
  _refActiveLamp.DimDown();
END_IF

The controller is therefore generic. If a new lamp type is defined, the controller remains unchanged.

Admittedly, this delegated the task of selecting the desired lamp type to the caller of FB_Controller. Now, it must create the various lamp types and pass them to the controller. This is a good approach if, for example, all elements are contained in a library. With the adjustments shown above, it is now possible to develop your own lamp types without having to make adjustments to the library.

Sample 2 (TwinCAT 3.1.4024) on GitHub

Optimization analysis

Although a function block and an interface have been added, the amount of program code has not increased. The code only needed to be reasonably restructured to eliminate the problems mentioned above. The result is a long-term sustainable program structure, which was divided into several consistently small artifacts with clear responsibilities. The UML diagram shows the new distribution very well:

(abstract elements are displayed in italics)

FB_Controller no longer has a fixed binding to the individual lamp types. Instead, the abstract function block FB_Lamp is accessed, which is passed to the controller via the refActiveLamp property. The individual lamp types are then accessed via this abstraction level.

The definition of the Dependency Inversion Principle

The Dependency Inversion Principle consists of two rules and is described very well in the book (Amazon advertising link *) Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Referring to the above example, the high-level module is the FB_Controller function block. It should not directly access low-level modules that contain details. The low-level modules are the individual lamp types.

Abstractions should not depend on details. Details should depend on abstractions.

The details are the individual methods offered by the respective lamp types. In the first example, FB_Controller depends on the details of all lamp types. If a change is made to a lamp type, the controller must also be adapted.

What exactly does the Dependency Inversion Principle invert?

In the first example, FB_Controller accesses the individual lamp types directly. This makes FB_Controller (higher level) dependent on the lamp types (lower level).

The Dependency Inversion Principle inverts this dependency. For this purpose, an additional abstraction level is introduced. The higher layer specifies what this abstraction layer looks like. The lower layers must meet these requirements. This changes the direction of the dependencies.

In the above example, this additional abstraction level was implemented by combining the abstract function block FB_Lamp and the interface I_Lamp.

Summary

With the Dependency Inversion Principle, there is a risk of overengineering. Not every coupling should be resolved. Where an exchange of function blocks is to be expected, the Dependency Inversion Principle can be of great help. Above, I gave an example of a library in which different function blocks are interdependent. If the user of the library wants to intervene in these dependencies, fixed dependencies would prevent this.

The Dependency Inversion Principle increases the testability of a system. FB_Controller can be tested completely independently of the individual lamp types. For the unit tests, an FB is created which is derived from FB_Lamp. This dummy FB contains only functions that are necessary for the tests of FB_Controller, and is also called a mocking object. Jakob Sagatowski introduces this concept in his post Mocking objects in TwinCAT.

In the next post, I will analyze and further optimize the sample program using the Single Responsibility Principle (SRP).

Holger Schwichtenberg: Neu in .NET 6 [16]: N:M-Abstraktion beim Reverse Engineering mit Entity Framework Core

Der OR-Mapper benötigt nun keine reinen N:M-Zwischentabellen mehr.

Holger Schwichtenberg: Neu in .NET 6 [15]: Direkte Speicherzugriffe

Nach den "rohen" Dateisystemzugriffe geht es nun um direkte Speicherzugriffe in .NET 6.

Holger Schwichtenberg: Neu in .NET 6 [14]: Direkte Dateizugriffe ohne Stream-Objekte

Die Serie zu den Neuerungen in .NET 6 behandelt im vierzehnten Teil die Klasse System.IO.RandomAccess

Code-Inside Blog: Redirect to HTTPS with a simple web.config rule

The scenario is easy: My website is hosted in an IIS and would like to redirect all incomming HTTP traffic to the HTTPS counterpart.

This is your solution - a “simple” rule:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="Redirect to https" stopProcessing="true">
                    <match url=".*" />
                    <conditions logicalGrouping="MatchAny">
                        <add input="{HTTPS}" pattern="off" />
                    </conditions>
                    <action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" redirectType="Found" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

We used this in the past to setup a “catch all” web site in an IIS that redirects all incomming HTTP traffic. The actual web applications had only the HTTPS binding in place.

Hope this helps!

Golo Roden: Log4j – warum Open Source kaputt ist

Im Dezember 2021 sorgte die Sicherheitslücke Log4Shell in dem Logging-Framework Log4j für Java für Aufregung, die vom BSI als kritisch eingeschätzt und auf Warnstufe Rot eingestuft wurde. Nachdem sich der Staub inzwischen gelegt hat, ist es an der Zeit für einen Review: Was lässt sich aus alldem lernen?

Holger Schwichtenberg: Neu in .NET 6 [13]: EnsureCapacity() für Objektmengen

Die neue Methode EnsureCapacity() reserviert Speicher in .NET-Objektmengen.

Jürgen Gutsch: Customizing ASP.NET Core 6.0 - The second edition

Just a couple of days ago, the second edition of my book Customizing ASP.NET Core got released by Packt

image-20220103222013400

The second edition is updated to .NET 6 and includes three new chapters. I also put the chapters into a more logical order :-D

This is the nee table of contents:

  1. Customizing Logging
  2. Customizing App Configuration
  3. Customizing Dependency Injection
  4. Configuring and Customizing HTTPS
  5. Configuring WebHostBuilder
  6. Using different Hosting models
  7. Using IHostedService and BackgroundService
  8. Writing Custom Middleware
  9. Working with Endpoint Routing
  10. Customizing ASP.NET Core Identity [NEW]
  11. Configuring Identity Management [NEW]
  12. Content Negotiation Using a Custom OutputFormatter
  13. Managing Inputs with Custom ModelBinders
  14. Creating custom ActionFilter
  15. Working with Caches [NEW]
  16. Creating custom TagHelpers

Working with Packt

I'd like to thank the Packt team and its motivation and accuracy to create the best result possible. Actually writing isn't that hard, but getting it completely right, nice, and readable afterward is the hard part. Packt did a great job and I really like the result.

Maybe the next project is in the making. ;-)

Martin Richter: Neue Version von PTZControl unterstützt jetzt direkt auch Logitech Rally Plus Kameras

Ich habe ein neue Version von meinem Kamerasteuerungs-Programm PTZControl bereitgestellt.

Was ist alles neu?

  1. Es werden automatisch Logitech PTZ2 Pro und Logitech Rally Plus Kameras erkannt.
  2. Ich habe eine komplette Readme Datei mit Hintergründen und allen notwendigen Beschreibungen erstellt (aktuell nur in Deutsch).
  3. Weitere neue Befehlszeilenschalter wurden eingebaut für einige Optionen um die Bedienung leichter zu machen.

Die neue Version findet Ihr hier unter dem folgenden Link.


Copyright © 2017 Martin Richter
Dieser Feed ist nur für den persönlichen, nicht gewerblichen Gebrauch bestimmt. Eine Verwendung dieses Feeds bzw. der hier veröffentlichten Beiträge auf anderen Webseiten bedarf der ausdrücklichen Genehmigung des Autors.
(Digital Fingerprint: bdafe67664ea5aacaab71f8c0a581adf)

Jürgen Gutsch: New blog sponsor - YOO inc.

I warmly welcome the YOO Inc. as a new sponsor of my blog:

The YOO Inc., is located in Basel, Switzerland and serves national as well as international clients and specializes in creating custom digital solutions for distinct business needs.

Actually, YOO Inc. is the first official Sponsor of my blog and is sponsoring for a few months already but I got that cool banner just a few weeks ago.

Anyway, Thank YOO!

I got rid of Google AdSense which anyway didn't made a lot of sense on my blog. The less dependencies the better.

Holger Schwichtenberg: Neu in .NET 6 [12]: PriorityQueue - Warteschlange mit Prioritäten

Die neue Klasse PriorityQueue bietet eine FIFO-Liste mit Prioritäten.

Martin Richter: Keine Umlaute und falsche Zeichen mit der deutschen Tastatur EJ-FT720 für das Samsung Tablet S5e

Ich habe schon länger ein Tab-S5e und habe mir eine EJ-FT720 Tastatur zugelegt (B-Ware). Wenn man manchmal etwas mehr schreibt ist das schon sehr hilfreich.

Kaum hate ich die Tastatur angeschlossen fingen die Probleme an.
Die Umlaute und alle Tasten mit Umschalt+Numerische Taste lieferten die Zeichen der englischen Tastatur.

Aber in Android war alles korrekt eingestellt. Deutsche Tastatur.

Lange musste ich suchen bis ich einen Hinweis fand.

Die Lösung ist relativ einfach:

Einstellungen -> Allgemeine Verwaltung -> Zurücksetzen -> Eingabehilfeeinstellungen zurücksetzen

Ich war mit gar nicht bewusst, dass ich hier etwas eingetragen oder verstellt habe. Letzten habe ich dann auch den Auslöser für das Problem gefunden:

In den Eingabehilfen kann man einen automatischen Zoom mit einem Dreichfach-Tap auslösen.
Wird diese Einstellung vorgenommen, dann geht die Tastatur nicht mehr.

Sollte also jemand wieder diese Zoom Einstellung einrichten (was ich wieder getan hatte), dann funktioniert sofort auch die Tastatur nicht mehr wie erwartet.


Copyright © 2017 Martin Richter
Dieser Feed ist nur für den persönlichen, nicht gewerblichen Gebrauch bestimmt. Eine Verwendung dieses Feeds bzw. der hier veröffentlichten Beiträge auf anderen Webseiten bedarf der ausdrücklichen Genehmigung des Autors.
(Digital Fingerprint: bdafe67664ea5aacaab71f8c0a581adf)

Holger Schwichtenberg: Neu in .NET 6 [11]: Standardparameter für FirstOrDefault() und Co.

Einige LINQ-Operationen bieten nun einen neuen Parameter, mit dem man einen Wert setzen kann, wenn kein Element gefunden wurde.

Holger Schwichtenberg: Neu in .NET 6 [10]: Ergänzungen in System.Environment

Die Serie zu den Neuerungen in .NET 6 behandelt im zehnten Teil den Zugang zu Informationen über laufende Prozesse.

Golo Roden: Eine weihnachtliche LED-Schneelandschaft im Eigenbau

Der Advent gilt als besinnliche und beschauliche Zeit. Warum also nicht sich die Zeit nehmen und gemeinsam mit der Familie oder Freunden etwas schönes basteln und entwickeln – beispielsweise eine weihnachtliche LED-Schneelandschaft?

Holger Schwichtenberg: Neu in .NET 6 [9]: Datentypen DateOnly und TimeOnly

Neben dem zusammengesetzten Datentyp DateTime kann man in .NET 6 nun auch Datum und Uhrzeit getrennt ablegen.

Code-Inside Blog: Select random rows

Let’s say we have a SQL table and want to retrieve 10 rows randomly - how would you do that? Although I have been working with SQL for x years, I have never encountered that problem. The solution however is quite “simple” (at least if you don’t be picky how we define “randomness” and if you try this on millions of rows):

ORDER BY NEWID()

The most boring way is to use the ORDER BY NEWID() clause:

SELECT TOP 10 FROM Products ORDER BY NEWID()

This works, but if you do that on “large” datasets you might hit performance problems (e.g. more on that here)

TABLESAMPE

The SQL Server implements the Tablesample clause which was new to me. It seems to perform much bettern then the ORDER BY NEWID() clause, but behaves a bit weird. With this clause you can specify the “sample” from a table. The size of the sample can be specified as PERCENT or ROWS (which are then converted to percent internally).

Syntax:

SELECT TOP 10 FROM Products TABLESAMPLE (25 Percent)
SELECT TOP 10 FROM Products TABLESAMPLE (100 ROWS)

The weird part is that the given number might not match the number of rows of your result. You might got more or less results and if our tablesample is too small you might even got nothing in return. There are some clever ways to work around this (e.g. using the TOP 100 statement with a much larger tablesample clause to get a guaranteed result set), but it feels “strange”. If you hit limitations with the first solution you might want to read more on this blog or in the Microsoft Docs.

Stackoverflow

Of course there is a great Stackoverflow thread with even wilder solutions.

Hope this helps!

Holger Schwichtenberg: Neu in .NET 6 [8]: Neuer LINQ-Operator IntersectBy()

Die Serie zu den Neuerungen in .NET 6 behandelt im achten Teil einen Operator zum Erstellen von Schnittmengen.

Holger Schwichtenberg: In eigener Sache: Restplätze bei Online-Workshops unter anderem zu Blazor verfügbar

www.IT-Visions.de bietet Restplätze bei mehreren interaktive Online-Schulungen an.

Martin Richter: Sammlung von Tastaturkürzeln meiner Samsung Tastatur an meinem Samsung Tablet

Es gibt bisher keine vernünftige Übersicht aller Tastatur Kombinationen auf den Bluetooth-Tastaturen/Tastaturen für ein Android Tablet.

Ich habe ein Samsung S Tablet und eine passende Samsung Tastatur. Ich habe mir mal die Mühe gemacht hier einige der Tastenkürzel zusammen zu stellen.

Die Betriebssystem-Taste wie Sie in der Samsung Beschreibung genannt wird kürze ich mit OS ab (bei meiner Tastatur ist eine Lupe abgebildet Lupe.

KürzelBedeutung
Strg+CKopieren in die Zwischenablage
Strg+XAusschneiden in die Zwischenablage
Strg+VEinfügen aus der Zwischenablage
Strg+ZRückgängig machen
Umschalt+
Cursor-Tasten
Zeichen markieren (erweitern)
Umschalt+Strg+
Cursor-Tasten
Wort markieren
Strg+0-9Wechselt in das entsprechende Tab in Chrome
Alt+TabApp wechseln
Alt+LinksIm Browser zurück
Alt+RechtsIm Browser vorwärts
Alt+EAdressleiste im Chrome Browser anspringen
OSGoogle Assistent öffnen
OS+TabApp Übersicht öffnen
OS+BackspaceZurück
OS+Ü / OS++Apps nebeneinander anzeigen / starten
OS+BBrowser App öffnen
OS+CKontakte öffnen
OS+DStart Bildschirm anzeigen
OS+EEmail-App starten
OS+FSuch Funktion öffnen
OS+IBixby App öffnen
OS+HSamsung Spracheingabe öffnen
OS+KKalender App öffnen
OS+LGerät sperren
OS+MGoogle Maps öffnen
OS+NBenachrichtigungen anzeigen
OS+PMusic-Player App öffnen
OS+QEinstellungsmenü
OS+RApp Eigene Dateien öffnen
OS+SSMS App
OS+WSamsung DeX starten/beenden
OS+UEinstellungen für Eingabehilfen öffnen
OS+VLautstärkeregelung öffnen
OS+XAktuelle App schließen
OS+YSmartview App öffnen
OS+ZAndroid Einstellungen öffnen
OS+ßTastaturkürzel Übersicht anzeigen (leider nicht vollständig)

Im großen und ganzen funktionieren viele Tastenkombinationen ähnlich wie auf einem Windows PC, besonders was das Editieren betrifft. Das betrifft besonders auch den Chrome Browser bei dem die Tastatur wirklich nützlich ist, wenn man gewöhnt ist den Browser mit der Tastatur zu bedienen.


Copyright © 2017 Martin Richter
Dieser Feed ist nur für den persönlichen, nicht gewerblichen Gebrauch bestimmt. Eine Verwendung dieses Feeds bzw. der hier veröffentlichten Beiträge auf anderen Webseiten bedarf der ausdrücklichen Genehmigung des Autors.
(Digital Fingerprint: bdafe67664ea5aacaab71f8c0a581adf)

Code-Inside Blog: SQL collation problems

This week I deployed a new feature and tried it on different SQL databases and was a bit suprised that on one database this error message came up:

Cannot resolve the collation conflict between "Latin1_General_CI_AS" and "SQL_Latin1_General_CP1_CI_AS" in the equal to operation.

This was strange, because - at least in theory - all databases have the same schema and I was sure that each database had the same collation setting.

Collations on columns

Well… my theory was wrong and this SQL statement told me that “some” columns had a different collation.

select sc.name, sc.collation_name from sys.columns sc
inner join sys.tables t on sc.object_id=t.object_id
where t.name='TABLENAME'

As it turns out, some columns had the collation Latin1_General_CI_AS and some had SQL_Latin1_General_CP1_CI_AS. I’m still not sure why, but I needed to do something.

How to change the collation

To change the collation you can execute something like this:

ALTER TABLE MyTable
ALTER COLUMN [MyColumn] NVARCHAR(200) COLLATE SQL_Latin1_General_CP1_CI_AS

Unfortunately there are restrictions and you can’t change the collation if the column is referenced by any one of the following:

  • A computed column
  • An index
  • Distribution statistics, either generated automatically or by the CREATE STATISTICS statement
  • A CHECK constraint
  • A FOREIGN KEY constraint

Be aware: If you are not in control of the collation or if the collation is “fine” and you want to do this operation anyway, there might be a way to specify the collation in the SQL query.

For more information you might want to check out this Microsoft Docs “Set or Change the Column Collation

Hope this helps!

Holger Schwichtenberg: Neu in .NET 6 [7]: Neuer LINQ-Operator UnionBy()

Die Serie zu den Neuerungen in .NET 6 behandelt im siebten Teil einen Operator zum Verbinden von Objektmengen ohne Duplikate.

Holger Schwichtenberg: Neu in .NET 6 [6]: LINQ-Operator DistinctBy()

Die Serie zu den Neuerungen in .NET 6 behandelt im sechsten Teil einen Operator zum Entfernen von Duplikaten.

Don't contact us via this (fleischfalle@alphasierrapapa.com) email address.