Code-Inside Blog: Azure DevOps & Azure Service Connection

Today I needed to setup a new release pipeline on our Azure DevOps Server installation to deploy some stuff automatically to Azure. The UI (at least on the Azure DevOps Server 2020 (!)) is not really clear about how to connect those two worlds, and thats why I’m writing this short blogpost.

First - under project settings - add a new service connection. Use the Azure Resource Manager-service. Now you should see something like this:

x

Be aware: You will need to register app inside your Azure AD and need permissions to setup. If you are not able to follow these instructions, you might need to talk to your Azure subscription owner.

Subscription id:

Copy here the id of your subscription. This can be found in the subscription details:

x

Keep this tab open, because we need it later!

Service prinipal id/key & tenant id:

Now this wording about “Service principal” is technically correct, but really confusing if your are not familar with Azure AD. A “Service prinipal” is like a “service user”/”app” that you need to register to use it. The easiest route is to create an app via the Bash Azure CLI:

az ad sp create-for-rbac --name DevOpsPipeline

If this command succeeds you should see something like this:

{
  "appId": "[...GUID..]",
  "displayName": "DevOpsPipeline",
  "password": "[...PASSWORD...]",
  "tenant": "[...Tenant GUID...]"
}

This creates an “Serivce principal” with a random password inside your Azure AD. The next step is to give this “Service principal” a role on your subscription, because it has currently no permissions to do anything (e.g. deploy a service etc.).

Go to the subscription details page and then to Access control (IAM). There you can add your “DevOpsPipeline”-App as “Contributor” (Be aware that this is a “powerful role”!).

After that use the "appId": "[...GUID..]" from the command as Service Principal Id. Use the "password": "[...PASSWORD...]" as Service principal key and the "tenant": "[...Tenant GUID...]" for the tenant id.

Now you should be able to “Verify” this connection and it should work.

Links: This blogpost helped me a lot. Here you can find the official documentation.

Hope this helps!

Jürgen Gutsch: ASP.NET Core 7 updates

Release candidate 1 of ASP.NET Core 7 is out for around two weeks and the release date isn't that far. The beginning of November usually is the time when Microsoft is releasing the new version of .NET. Please find the announcement post here: ASP.NET Core updates in .NET 7 Release Candidate 1. I will not repeat this post but pick some personal highlights to write about.

ASP.NET Core Roadmap for .NET 7

First of all, a look at the ASP.NET Core roadmap for .NET 7 shows us, that there are only a few issues open and planned for the upcoming release. That means the release is complete and almost only bugfixes will be pushed to that release. Many other open issues are already stroked through and probably assigned to a later release. Guess we'll have a published roadmap for ASP.NET Core on .NET 8 soon. At the latest at the beginning of November.

What are the updates of this RC 1?

A lot of Blazor

Even this release is full of Blazor improvements. Those working a lot with Blazor will be happy about improved JavaScript interop, debugging improvements, handling location-changing events, and dynamic authentication requests coming with this release.

However, there are some quite interesting improvements within this release that might be great for almost every ASP.NET Core developer:

Faster HTTP/2 uploads and HTTP3 performance improvements

The team increases the default upload connection window size of HTTP/2, resulting in a much faster upload time. Stream handling is always tricky and needs a lot of fine-tuning to find the right balance. Improving the upload speed by more than five times is awesome and really helpful to upload bigger files. Even in HTTP/3 the performance was increased by reducing HTTP/3 allocations. Feature parity with HTTP/1, HTTP/2, and HTTP/3 is as useful as Server Name Indication (SNI) when configuring connection certificates.

Rate limiting middleware improvements

The rate-limiting middleware got some small improvements to make it easier and more flexible to configure. You can now add attributes to controller actions to enable or disable rate limiting on specific endpoints. To do the same on Minimal API endpoints and endpoint groups you can use methods to enable or disable rate limiting. This way you can enable rate-limiting for an endpoint group, but disable it for a specific one inside this group.

You can specify the rate limiting policy on both attributes, endpoints, and endpoint groups methods. Unlike the attributes that support named policies, only the Minimal API methods can also take an instance of a policy.

Experimental stuff added to this release

WebTransport is a new draft specification for HTTP/3 that works similarly to WebSockets but supports multiple streams per connection. The support for WebTransport is now added as an experimental feature to the RC1

One of the new features in .NET 7 is gRPC JSON transcoding to turn gRPC APIs into RESTful APIs. Any RESTful API should have an OpenAPI documentation, and so should gRPC JSON transcoding. This release now contains experimental support to add Swashbuckle Swagger to gRPC to render an OpenAPI documentation

Conclusion

ASP.NET Core on .NET 7 seems to be complete now and I'm really looking forward to the .NET Conf 2022 beginning of November which will be the launch event for .NET 7.

And exactly this reminds me to start thinking about the next edition of my book "Customizing ASP.NET Core" which needs to be updated to .NET 8 and enhanced by probably three more chapters next year.

Stefan Henneken: IEC 61131-3: SOLID – Das Interface Segregation Principle

Der Grundgedanke des Interface Segregation Principle (ISP) hat starke Ähnlichkeit mit dem Single Responsibility Principle (SRP): Module mit zu vielen Zuständigkeiten können die Pflege und Wartbarkeit eines Softwaresystem negativ beeinflussen. Das Interface Segregation Principle (ISP) legt den Schwerpunkt hierbei auf die Schnittstelle des Moduls. Ein Modul sollte nur die Schnittstellen implementieren, die für seine Aufgabe benötigt werden. Im Folgenden wird gezeigt, wie dieses Designprinzip umgesetzt werden kann.

Ausgangssituation

Im letzten Post (IEC 61131-3: SOLID – Das Liskov Substitution Principle) wurde das Beispiel um einen weiteren Lampentyp (FB_LampSetDirectDALI) erweitert. Das Besondere an diesem Lampentyp ist die Skalierung des Ausgangwertes. Während die anderen Lampentypen 0-100 % ausgeben, gibt der neue Lampentyp einen Wert von 0 bis 254 aus.

So wie alle anderen Lampentypen, besitzt auch der neue Lampentyp (DALI-Lampe) einen Adapter (FB_LampSetDirectDALIAdapter). Die Adapter sind bei der Umsetzung des Single Responsibility Principle (SRP) hinzugekommen und stellen sicher, dass die Funktionsblöcke der einzelnen Lampentypen nur für eine einzelne Fachlichkeit zuständig sind (siehe IEC 61131-3: SOLID – Das Single Responsibility Principle).

Das Beispielprogramm wurde zuletzt so angepasst, dass von dem neuen Lampentyp (FB_LampSetDirectDALI) der Ausgangswert innerhalb des Adapters von 0-254 auf 0-100 % skaliert wird. Dadurch verhält sich die DALI-Lampe genau wie die anderen Lampentypen, ohne das Liskov Substitution Principle (LSP) zu verletzen.

Dieses Beispielprogramm soll uns als Ausgangssituation für die Erklärung des Interface Segregation Principle (ISP) dienen.

Erweiterung der Implementierung

Auch dieses Mal, soll die Anwendung erweitert werden. Allerdings wird nicht ein neuer Lampentyp definiert, sondern ein vorhandener Lampentyp wird um eine Funktionalität erweitert. Die DALI-Lampe soll in der Lage sein, die Betriebsstunden zu zählen. Hierzu wird der Funktionsblock FB_LampSetDirectDALI um die Eigenschaft nOperatingTime erweitert.

PROPERTY PUBLIC nOperatingTime : DINT

Über den Setter kann der Betriebsstundenzähler auf einen beliebigen Wert gesetzt werden, während der Getter den aktuellen Zustand des Betriebsstundenzählers zurückgibt.

Da FB_Controller die einzelnen Lampentypen repräsentiert, wird dieser Funktionsblock ebenfalls um nOperatingTime erweitert.

Die Erfassung der Betriebsstunden erfolgt im Funktionsblock FB_LampSetDirectDALI. Ist der Ausgangswert > 0, so wird jede Sekunde der Betriebsstundenzähler um 1 erhöht:

IF (nLightLevel > 0) THEN
  tonDelay(IN := TRUE, PT := T#1S);
  IF (tonDelay.Q) THEN
    tonDelay(IN := FALSE);
    _nOperatingTime := _nOperatingTime + 1;
  END_IF
ELSE
  tonDelay(IN := FALSE);
END_IF

Die Variable _nOperatingTime ist die Backing Variable für die neue Eigenschaft nOperatingTime und ist im Funktionsblock deklariert.

Welche Möglichkeiten gibt es, um den Wert von nOperatingTime aus FB_LampSetDirectDALI in die Eigenschaft nOperatingTime von FB_Controller zu übertragen? Auch hier gibt es jetzt verschiedene Ansätze, um die geforderte Erweiterung in die gegebene Softwarestruktur zu integrieren.

Ansatz 1: Erweiterung von I_Lamp

Die Eigenschaft für das neue Leistungsmerkmal wird mit in die Schnittstelle I_Lamp integriert. Somit erhält auch der abstrakte Funktionsblock FB_Lamp die Eigenschaft nOperatingTime. Da alle Adapter von FB_Lamp erben, erhalten die Adapter aller Lampentypen diese Eigenschaft, unabhängig ob der Lampentyp einen Betriebsstundenzähler unterstützt oder nicht.

Der Getter und der Setter von nOperatingTime in FB_Controller können somit direkt auf nOperatingTime der einzelnen Adapter der Lampentypen zugreifen. Der Getter von FB_Lamp (abstrakter Funktionsblock, von dem alle Adapter erben) liefert den Wert -1 zurück. Somit kann das Fehlen des Betriebsstundenzähler erkannt werden.

IF (fbController.nOperatingTime >= 0) THEN
  nOperatingTime := fbController.nOperatingTime;
ELSE
  // service not supported
END_IF

Da FB_LampSetDirectDALI den Betriebsstundenzähler unterstützt, überschreibt der Adapter (FB_LampSetDirectDALIAdapter) die Eigenschaft nOperatingTime. Der Getter und der Setter vom Adapter greifen auf nOperatingTime von FB_LampSetDirectDALI zu. Der Wert des Betriebsstundenzählers wird somit bis zu FB_Controller weitergegeben.

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 1 (TwinCAT 3.1.4024) auf GitHub

Dieser Ansatz setzt das Leistungsmerkmal wie gewünscht um. Auch werden keine der bisher gezeigten SOLID-Prinzipen verletzt.

Allerdings wird die zentrale Schnittstelle I_Lamp erweitert, nur um bei einem Lampentyp ein weiteres Leistungsmerkmal hinzuzufügen. Alle anderen Adapter der Lampentypen, auch die, die das neue Leistungsmerkmal nicht unterstützen, erhalten über den abstrakten Basis-FB FB_Lamp ebenfalls die Eigenschaft nOperatingTime.

Mit jedem Leistungsmerkmal, welches auf diese Weise hinzugefügt wird, vergrößert sich die Schnittstelle I_Lamp und somit auch der abstrakte Basis-FB FB_Lamp.

Ansatz 2: zusätzliche Schnittstelle

Bei diesem Ansatz wird die Schnittstelle I_Lamp nicht erweitert, sondern es wird für die gewünschte Funktionalität eine neue Schnittstelle (I_OperatingTime) hinzugefügt. I_OperatingTime enthält nur die Eigenschaft, die für das Bereitstellen des Betriebsstundenzählers notwendig ist:

PROPERTY PUBLIC nOperatingTime : DINT

Implementiert wird diese Schnittstelle vom Adapter FB_LampSetDirectDALIAdapter.

FUNCTION_BLOCK PUBLIC FB_LampSetDirectDALIAdapter EXTENDS FB_Lamp IMPLEMENTS I_OperatingTime

Somit erhält FB_LampSetDirectDALIAdapter die Eigenschaft nOperationTime nicht über FB_Lamp bzw. I_Lamp, sondern über die neue Schnittstelle I_OperatingTime.

Greift FB_Controller im Getter von nOperationTime auf den aktiven Lampentyp zu, so wird vor dem Zugriff geprüft, ob der ausgewählte Lampentyp die Schnittstelle I_OperatingTime implementiert. Ist dieses der Fall, so wird über I_OperatingTime auf die Eigenschaft zugegriffen. Hat der Lampentyp die Schnittstelle nicht implementiert, wird -1 zurückgegeben.

VAR
  ipOperatingTime  : I_OperatingTime;
END_VAR
IF (__ISVALIDREF(_refActiveLamp)) THEN
  IF (__QUERYINTERFACE(_refActiveLamp, ipOperatingTime)) THEN
    nOperatingTime := ipOperatingTime.nOperatingTime;
  ELSE
    nOperatingTime := -1; // service not supported
  END_IF
END_IF

Ähnlich ist der Setter von nOperationTime aufgebaut. Nach der erfolgreichen Prüfung, ob I_OperatingTime von der aktiven Lampe implementiert wird, erfolgt über die Schnittstelle der Zugriff auf die Eigenschaft.

VAR
  ipOperatingTime  : I_OperatingTime;
END_VAR
IF (__ISVALIDREF(_refActiveLamp)) THEN
  IF (__QUERYINTERFACE(_refActiveLamp, ipOperatingTime)) THEN
    ipOperatingTime.nOperatingTime := nOperatingTime;
  END_IF
END_IF
(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 2 (TwinCAT 3.1.4024) auf GitHub

Analyse der Optimierung

Das Verwenden einer separaten Schnittstelle für das zusätzliche Leistungsmerkmal entspricht der ‚Optionalität‘ aus IEC 61131-3: SOLID – Das Liskov Substitution Principle. In dem obigen Beispiel kann zur Laufzeit des Programms geprüft werden (mit __QUERYINTERFACE()), ob eine bestimmte Schnittstelle implementiert und somit das jeweilige Leistungsmerkmal unterstützt wird. Weitere Eigenschaften, wie bIsDALIDevice aus dem ‚Optionalität‘-Beispiel, sind bei diesem Lösungsansatz nicht notwendig.

Wird pro Leitungsmerkmal bzw. Funktionalität eine separate Schnittstelle angeboten, können andere Lampentypen diese ebenfalls implementieren, um so das gewünschte Leistungsmerkmal umzusetzen. Soll FB_LampSetDirect ebenfalls einen Betriebsstundenzähler erhalten, so muss FB_LampSetDirect um die Eigenschaft nOperatingTime erweitert werden. Außerdem muss FB_LampSetDirectAdapter die Schnittstelle I_OperatingTime implementieren. Alle anderen Funktionsblöcke, auch FB_Controller, bleiben unverändert.

Ändert sich die Funktionsweise der Betriebsstundenzähler und I_OperatingTime erhält zusätzliche Methoden, so müssen nur die Funktionsblöcke angepasst werden, die auch das Leistungsmerkmal unterstützen.

Beispiele für das Interface Segregation Principle (ISP) sind auch im .NET zu finden. So gibt es in .NET die Schnittstelle IList. Diese Schnittstelle enthält Methoden und Eigenschaften für das Anlegen, Verändern und Lesen von Auflistungen. Je nach Anwendungsfall ist es aber ausreichend, dass der Anwender eine Auflistung nur lesen muss. Das Übergeben einer Auflistung durch IList würde in diesem Fall aber auch Methoden anbieten, um die Auflistung zu verändern. Für diese Anwendungsfälle gibt es die Schnittstelle IReadOnlyList. Mit dieser Schnittstelle kann eine Auflistung nur gelesen werden. Ein versehentliches Verändern der Daten ist somit nicht möglich.

Das Aufteilen von Fachlichkeiten in einzelne Schnittstellen erhöht somit nicht nur die Wartbarkeit, sondern auch die Sicherheit eines Softwaresystems.

Die Definition des Interface Segregation Principle

Damit kommen wir auch schon zur Definition des Interface Segregation Principle (ISP):

Ein Modul, das eine Schnittstelle benutzt, sollte nur diejenigen Methoden präsentiert bekommen, die sie auch wirklich benötigt.

Oder etwas anders formuliert:

Clients sollten nicht gezwungen werden, von Methoden abhängig zu sein, die sie nicht benötigen.

Ein häufiges Argument gegen das Interface Segregation Principle (ISP) ist die erhöhte Anzahl von Schnittstellen. Ein Softwareentwurf kann im Laufe seiner Entwicklungszyklen jederzeit noch angepasst werden. Wenn Sie also das Gefühl haben, das eine Schnittstelle zu viele Funktionalitäten beinhaltet, prüfen Sie, ob eine Aufteilung möglich ist. Natürlich sollte ein Overengineering immer vermieden werden. Ein gewisses Maß an Erfahrung kann hierbei hilfreich sein.

Abstrakte Funktionsblöcke stellen ebenfalls eine Schnittstelle (siehe FB_Lamp) dar. In einem abstrakten Funktionsblock können Grundfunktionen enthalten sein, die der Anwender nur um die notwendigen Details ergänzt. Es ist nicht notwendig, alle Methoden oder Eigenschaften selbst zu implementieren. Aber auch hierbei ist es wichtig, den Anwender nicht mit Fachlichkeiten zu belasten, die für seine Aufgaben nicht notwendig sind. Die Menge der abstrakten Methoden und Eigenschaften sollte möglichst klein sein.

Die Beachtung des Interface Segregation Principles (ISP) hält Schnittstellen zwischen Funktionsblöcken so klein wie möglich, wodurch die Kopplung zwischen den einzelnen Funktionsblöcken reduziert wird.

Zusammenfassung

Soll ein Softwaresystem weitere Leistungsmerkmale abdecken, so reflektieren Sie die neuen Anforderungen und erweitern Sie nicht voreilig bestehende Schnittstellen. Prüfen Sie, ob separate Schnittstellen nicht die bessere Entscheidung sind. Als Belohnung erhalten Sie ein Softwaresystem das leichter zu pflegen, besser zu testen und einfacher zu erweitern ist.

Im Letzten noch ausstehenden Teil, wird das Open Closed Principle (OCP) näher erklärt.

Stefan Henneken: IEC 61131-3: SOLID – The Liskov Substitution Principle

„The Liskov Substitution Principle (LSP) requires that derived function blocks (FBs) are always compatible to their base FB. Derived FBs must behave like their respective base FB. A derived FB may extend the base FB, but not restrict it.” This is the core statement of the Liskov Substitution Principle (LSP), which Barbara Liskov formulated already in the late 1980s. Although the Liskov Substitution Principle (LSP) is one of the simpler SOLID principles, its violation is very common. The following example shows why the Liskov Substitution Principle (LSP) is important.

Starting situation

I use once again the example, which was already developed and optimized in the two previous posts. The core of the example are three lamp types, which are mapped by the function blocks FB_LampOnOff, FB_LampSetDirect and FB_LampUpDown. The interface I_Lamp and the abstract function block FB_Lamp secure a clear decoupling between the respective lamp types and the higher-level controller FB_Controller.

FB_Controller no longer accesses specific instances, but only a reference of the abstract function block FB_Lamp. The IEC 61131-3: SOLID – The Dependency Inversion Principle is used to break the fixed coupling.

To realize the required functionality, each lamp type provides its own methods. For this reason, each lamp type also has a corresponding adapter function block (FB_LampOnOffAdapter, FB_LampSetDirectAdapter and FB_LampUpDownAdapter), which is responsible for mapping between the abstract lamp (FB_Lamp) and the concrete lamp types (FB_LampOnOff, FB_LampSetDirect and FB_LampUpDown). This optimization is supported by the IEC 61131-3: SOLID – The Single Responsibility Principle.

Extension of the implementation

The three required lamp types can be mapped well by the existing software design. Nevertheless, it can happen that extensions, which seem simple at first sight, lead to difficulties later. The new lamp type FB_LampSetDirectDALI will serve as an example here.

DALI stands for Digital Addressable Lighting Interface and is a protocol for controlling lighting devices. Basically, the new function block behaves like FB_LampSetDirect, but with DALI the output value is not given in 0-100 % but in 0-254.

Optimization and analysis of the extensions

Which approaches are available to implement this extension? The different approaches will also be analyzed in more detail.

Approach 1: Quick & Dirty

High time pressure can tempt to realize the Quick & Dirty implementation. Since FB_LampSetDirect behaves similarly to the new DALI lamp type, FB_LampSetDirectDALI inherits from FB_LampSetDirect. To enable the value range of 0-254, the SetLightLevel() method of FB_LampSetDirectDALI is overwritten.

METHOD PUBLIC SetLightLevel
VAR_INPUT
  nNewLightLevel : BYTE(0..254);
END_VAR
nLightLevel := nNewLightLevel;

The new adapter function block (FB_LampSetDirectDALIAdapter) is also adapted so that the methods regard the value range 0-254.

As an example, the methods DimUp() and On() are shown here:

METHOD PUBLIC DimUp
IF (fbLampSetDirectDALI.nLightLevel <= 249) THEN
  fbLampSetDirectDALI.SetLightLevel(fbLampSetDirectDALI.nLightLevel + 5);
END_IF
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(fbLampSetDirectDALI.nLightLevel);
END_IF
METHOD PUBLIC On
fbLampSetDirectDALI.SetLightLevel(254);
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(fbLampSetDirectDALI.nLightLevel);
END_IF

The simplified UML diagram shows the integration of the function blocks for the DALI lamp into the existing software design:

(abstract elements are displayed in italics)

Sample 1 (TwinCAT 3.1.4024) on GitHub

This approach implements the requirements quickly and easily through a pragmatic strategy. But this also added some specifics that complicate the use of the blocks in an application.

For example, how should a user interface behave when it connects to an instance of FB_Controller and FB_AnalogValue outputs a value of 100? Does 100 mean that the current lamp is at 100 % or does the new DALI lamp output a value of 100, which would be well below 100 %?

The user of FB_Controller must always know the active lamp type in order to interpret the current output value correctly. FB_LampSetDirectDALI inherits from FB_LampSetDirect, but changes its behavior. In this example, the behavior is changed by overwriting the SetLightLevel() method. The derived FB (FB_LampSetDirectDALI) behaves differently to the base FB (FB_LampSetDirect). FB_LampSetDirect can no longer be replaced (substituted) by FB_LampSetDirectDALI. The Liskov Substitution Principle (LSP) is violated.

Approach 2: Optionality

In this approach, each lamp type contains a property that returns information about the exact function of the function block.

In .NET, for example, this approach is used in the abstract class System.IO.Stream. The Stream class serves as the base class for specialized streams (e.g., FileStream and NetworkStream) and specifies the most important methods and properties. This includes the methods Write(), Read() and Seek(). Since not every stream can provide all functions, the properties CanRead, CanWrite and CanSeek provide information about whether the corresponding method is supported by the respective stream. For example, NetworkStream can check at runtime whether writing to the stream is possible or whether it is a read-only stream.

In our example, I_Lamp is extended by the property bIsDALIDevice.

This means that FB_Lamp and therefore every adapter function block also receives this property. Since the functionality of bIsDALIDevice is the same in all adapter function blocks, bIsDALIDevice is not declared as abstract in FB_Lamp. This means that it is not necessary for all adapter function blocks to implement this property themselves. The functionality of bIsDALIDevice is inherited by FB_Lamp to all adapter function blocks.

For FB_LampSetDirectDALIAdapter, the backing variable of the property bIsDALIDevice is set to TRUE in the method FB_init().

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains  : BOOL;
  bInCopyCode   : BOOL;
END_VAR
SUPER^._bIsDALIDevice := TRUE;

For all other adapter function blocks, _bIsDALIDevice retains its initialization value (FALSE). The use of the FB_init() method is not necessary for these adapter function blocks.

The user of FB_Controller (MAIN block) can now query at program runtime whether the current lamp is a DALI lamp or not. If this is the case, the output value is scaled accordingly to 0-100 %.

IF (__ISVALIDREF(fbController.refActiveLamp) AND_THEN fbController.refActiveLamp.bIsDALIDevice) THEN
  nLightLevel := TO_BYTE(fbController.fbActualValue.nValue * 100.0 / 254.0);
ELSE
  nLightLevel := fbController.fbActualValue.nValue;
END_IF

Note: It is important to use the AND_THEN operator instead of THEN. This means that the expression to the right of AND_THEN is only executed if the first operand (to the left of AND_THEN) is TRUE. This is important here because otherwise the expression fbController.refActiveLamp.bIsDALIDevice would terminate the execution of the program in case of an invalid reference to the active lamp (refActiveLamp).

The UML diagram shows how FB_Lamp receives the property bIsDALIDevice via the interface I_Lamp and is thus inherited by all adapter function blocks:

(abstract elements are displayed in italics)

Sample 2 (TwinCAT 3.1.4024) on GitHub

This approach still violates the Liskov Substitution Principle (LSP). FB_LampSetDirectDALI behaves further on differently to FB_LampSetDirect. The user hast to take this difference into account (querying bIsDALIDevice) and correct it (scaling to 0-100 %). This is easy to overlook or to implement incorrectly.

Approach 3: Harmonization

In order not to violate the Liskov Substitution Principle (LSP) any further, the inheritance between FB_LampSetDirect and FB_LampSetDirectDALI is resolved. Even if both function blocks appear very similar at first glance, the inheritance should be avoided with at this point.

The adapter function blocks ensure that all lamp types can be controlled using the same methods. However, there are still differences in the representation of the output value.

In FB_Controller the initial value of the active lamp is represented by an instance of FB_AnalogValue. A new initial value is transmitted by the Update() method. To ensure that the initial value is also displayed uniformly, it is scaled to 0-100 % before the Update() method is called. The necessary adjustments are made exclusively in the methods DimDown(), DimUp(), Off() and On() of FB_LampSetDirectDALIAdapter.

The On() method is shown here as an example:

METHOD PUBLIC On
fbLampSetDirectDALI.SetLightLevel(254);
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(TO_BYTE(fbLampSetDirectDALI.nLightLevel * 100.0 / 254.0));
END_IF

The adapter function block contains all the necessary instructions, which causes the DALI lamp to behave to the outside as expected. FB_LampSetDirectDALI remains unchanged with this solution approach.

(abstract elements are displayed in italics)

Sample 3 (TwinCAT 3.1.4024) on GitHub

Optimization analysis

Through various techniques, it is possible for us to implement the desired extension without violating the Liskov Substitution Principle (LSP). Inheritance is a precondition to violate the LSP. If the LSP is violated, this may be an indication of a bad inheritance hierarchy within the software design.

Why is it important to follow the Liskov Substitution Principle (LSP)? Function blocks can also be passed as parameters. If a POU would expect a parameter of the type FB_LampSetDirect, then FB_LampSetDirectDALI could also be passed when using inheritance. However, the operation of the SetLightLevel() method is different for the two function blocks. Such differences can lead to undesirable behavior within a system.

The definition of the Liskov Substituation Principle

Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.

This is the more formal definition of the Liskov Substitution Principle (LSP) by Barbara Liskov. As mentioned above, this principle was already defined at the end of the 1980s. The complete elaboration was published under the title Data Abstraction and Hierarchy.

Barbara Liskov was one of the first women to earn a doctorate in computer science in 1968. In 2008, she was also one of the first women to receive the Turing Award. Early on, she became involved with object-oriented programming and thus also with the inheritance of classes (function blocks).

Inheritance places two function blocks in a specific relationship to each other. Inheritance here describes an is-a relationship. If FB_LampSetDirectDALI inherits from FB_LampSetDirect, the DALI lamp is a (normal) lamp extended by special (additional) functions. Wherever FB_LampSetDirect is used, FB_LampSetDirectDALI could also be used. FB_LampSetDirect can be substituted by FB_LampSetDirectDALI. If this is not ensured, the inheritance should be questioned at this point.

Robert C. Martin has included this principle in the SOLID principles. In the book (Amazon advertising link *) Clean Architecture: A Craftsman’s Guide to Software Structure and Design, this principle is explained further and extended to the field of software architecture.

Summary

By extending the above example, you have learned about the Liskov Substitution Principle (LSP). Complex inheritance hierarchies in particular are prone to violating this principle. Although the formal definition of the Liskov Substitution Principle (LSP) sounds complicated, the key message of this principle is simple to understand.

In the next post, our example will be extended again. The Interface Segregation Principle (ISP) will play a central role in it.

Norbert Eder: Git Commit signieren – No secret key

Mit der Signatur des Commits unterschreibst du den Commit persönlich und bestätigst, dass der übermittelte Code von dir stammt. Das kann nur machen, wer auch den privaten Schlüssel zur Verfügung hast. In der Regel bist das ausschließlich du. Damit kann zwar jemand mit deinem Namen und Mail-Adresse einen Commit erstellen und pushen und sich als Du ausgeben, nicht aber mit deiner Signatur unterschreiben (Zugriff auf das Repository vorausgesetzt).

Nun kommt es aber nach der Konfiguration unter Windows häufig zu diesem Fehler:

gpg: signing failed: No secret key

In diesem fall fehlt dir vermutlich nur eine Git-Konfiguration. Und zwar kann Git die GPG-Applikation nicht finden.

git config --global gpg.program [GPG-Pfad]

Einfach den GPG-Pfad mit dem direkten Pfad zur gpg.exe versehen und das Signieren funktioniert sofort.

Wie Git-Commits signieren?

Falls sich die allgemeine Frage auftut, wie man Git-Commits signieren kann, ein paar kurze Worte dazu. Wenn du beispielsweise GPG verwendest, kannst du (sofern noch nicht vorhanden) ein neues Schlüsselpaar anlegen. Mit

gpg --list-keys

kannst du dir dann dein Schlüsselpaare ausgeben lassen. Vom gewünschten Schlüsselpaar kopierst du dir die Schlüssel-Id. Diese hinterlegst du in der Git-Konfiguration als Standard-Schlüssel-Id. Hierzu nachfolgend einfach [KEYID] mit dre Schüssel-Id ersetzen.

git config --global user.signingkey [KEYID]

Nun noch unter Windows den Link zur gpg.exe eintragen, wie oben gezeigt und schon können Commits mit der zusätzlichen Option -S des Kommandos git commit signiert werden. Hierzu unbedingt auf Groß- und Kleinschreibung achten. Beispiel:

git commit -S -am "Test commit"

So einfach fügst du deine persönliche Unterschrift dem Commit hinzu. Ich persönlich empfehle diese Vorgehensweise.

Der Beitrag Git Commit signieren – No secret key erschien zuerst auf Norbert Eder.

Norbert Eder: Abhängigkeiten und Vulnerabilities im Griff

Umso größer Entwicklungsprojekte sind, umso mehr Abhängigkeiten bestehen. Alle Abhängigkeiten im Überblick zu behalten ist teilweise schon eine Herausforderung, ganz zu schweigen, von der allzu oft durchgeführten wahllosen Einbindung ohne Check der Lizenzen, Vulnerabilities etc. im Vorfeld. Aber wie bekommt man diese Themen alle in den Griff?

Prüfen auf Vulnerabilities

Die meisten Paketmanager etc. bieten mittlerweile entsprechende Features an. Mit dotnet list package --vulnerable erfolgt beispielsweise im .NET-Umfeld eine Auflistung aller vulnerablen Pakete. Mit npm audit kann eine derartige Liste mit NPM herausgefahren werden.

Diesen Varianten ist aber gemein, dass sie den Status zum Aufrufzeitpunkt abbilden. Nicht mehr und nicht weniger. Und möglicherweise möchte man etwas mehr:

  • Tracking der Abhängigkeiten über Versionen der eigenen Software hinweg
  • Übersicht aller Lizenzen der Abhängigkeiten
  • Auflistung und Risikobewertung aller Schwachstellen pro Version der eigenen Software
  • Möglichkeit, Schwachstellen zu auditieren und Entscheidungen zu dokumentieren
  • Automatische Aktualisierung/Auswertung durch Integration ins Build-System

Dependency Track von OWASP

Das Open Web Application Security Project (kurz OWASP) ist vielen vielleicht ein Begriff, bringt die Foundation doch regelmäßig die Top 10 Web Application Security Risks heraus. Diese sollten in der Webentwicklung auf jeden Fall neben den Secure Coding Practices [PDF] und dem Web Security Testing Guide im Auge behalten werden.

Mit Dependency-Track stellt OWASP ein Tool zur Verfügung, in welches mittels einer CycloneDX-BOM (Bill of Material) Listen von Abhängigkeiten importiert und gegen Vulnerability Datenbanken geprüft werden. Hierfür stehen VulnDB, GitHub Advisories und zahlreiche weitere Quellen zur Verfügung.

Für die Generierung der notwendigen BOM stehen zahlreiche Tools für unterschiedliche Entwicklungsplattformen zur Verfügung. Somit ist eine einfache Einbindung in die Buildumgebung problemlos zu machen.

Die Installation von Dependency-Track gestaltet sich denkbar einfach, da die Auslieferung unter anderem als Docker-Container erfolgt.

Nachfolgend einige Screenshots des Herstellers.

Dependency-Track: Übersicht der Komponenten
Dependency-Track: Audit der gefundenen Vulnerabilities

Zudem steht ein übersichtliches Dashboard für einen Überblick über die gesamte Softwareinfrastruktur und einer Bewertung des aktuellen Risikos bereit.

Dependency-Track: Dashboard

Dependency-Track steht auf GitHub zur Verfügung und bereichert die Entwicklungsumgebung kostenlos.

Aktives Abhängigkeiten- und Schwachstellen-Management notwendig

Der bloße Einsatz dieses Tools bringt keine Verbesserung der Situation. Vielmehr muss es einen klaren Verantwortlichen geben, der zum Einen ein Abhängigkeitsmanagement betreibt (Wildwuchs eingrenzen, Überblick, Lizenzen) und zum anderen ein Audit über gefundene Risiken durchführt und deren Behebung (Aktualisierung der Abhängigkeit, Austausch etc.) einleitet.

Umso zentraler dieses Thema im Entwicklungsprozess behandelt wird, umso besser und schneller kann auf Schwachstellen reagiert werden.

Der Beitrag Abhängigkeiten und Vulnerabilities im Griff erschien zuerst auf Norbert Eder.

Holger Schwichtenberg: Droht nun das große Namenschaos beim OR-Mapper Entity Framework Core?

Mit oder ohne Core im Namen? Das ist die Frage, die sich beim Blick auf die Historie des Entity Framework (Core) 7 stellt.

Code-Inside Blog: 'error MSB8011: Failed to register output.' & UTF8-BOM files

Be aware: I’m not a C++ developer and this might be an “obvious” problem, but it took me a while to resolve this issue.

In our product we have very few C++ projects. We use these projects for very special Microsoft Office COM stuff and because of COM we need to register some components during the build. Everything worked as expected, but we renamed a few files and our build broke with:

C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\VC\v170\Microsoft.CppCommon.targets(2302,5): warning MSB3075: The command "regsvr32 /s "C:/BuildAgentV3_1/_work/67/s\_Artifacts\_ReleaseParts\XXX.Client.Addin.x64-Shims\Common\XXX.Common.Shim.dll"" exited with code 5. Please verify that you have sufficient rights to run this command. [C:\BuildAgentV3_1\_work\67\s\XXX.Common.Shim\XXX.Common.Shim.vcxproj]
C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Microsoft\VC\v170\Microsoft.CppCommon.targets(2314,5): error MSB8011: Failed to register output. Please try enabling Per-user Redirection or register the component from a command prompt with elevated permissions. [C:\BuildAgentV3_1\_work\67\s\XXX.Common.Shim\XXX.Common.Shim.vcxproj]

(xxx = redacted)

The crazy part was: Using an older version of our project just worked as expected, but all changes were “fine” from my point of view.

After many, many attempts I remembered that our diff tool doesn’t show us everything - so I checked the file encodings: UTF8-BOM

Somehow if you have a UTF8-BOM encoded file that your C++ project uses to register COM stuff it will fail. I changed the encoding and to UTF8 and everyting worked as expected.

What a day… lessons learned: Be aware of your file encodings.

Hope this helps!

Code-Inside Blog: Which .NET Framework Version is installed on my machine?

If you need to know which .NET Framework Version (the “legacy” .NET Framework) is installed on your machine try this handy oneliner:

Get-ItemProperty "HKLM:SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"

Result:

CBS           : 1
Install       : 1
InstallPath   : C:\Windows\Microsoft.NET\Framework64\v4.0.30319\
Release       : 528372
Servicing     : 0
TargetVersion : 4.0.0
Version       : 4.8.04084
PSPath        : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework
                Setup\NDP\v4\Full
PSParentPath  : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4
PSChildName   : Full
PSDrive       : HKLM
PSProvider    : Microsoft.PowerShell.Core\Registry

The version should give you more then enough information.

Hope this helps!

Christian Dennig [MS]: ASP.NET Custom Metrics with OpenTelemetry Collector & Prometheus/Grafana

Every now and then, I am asked which tracing, logging or monitoring solution you should use in a modern application, as the possibilities are getting more and more every month – at least, you may get that feeling. To be as flexible as possible and to rely on open standards, a closer look at OpenTelemetry is recommended. It becomes more and more popular, because it offers a vendor-agnostic solution to work with telemetry data in your services and send them to the backend(s) of your choice (Prometheus, Jaeger, etc.). Let’s have a look at how you can use OpenTelemetry custom metrics in an ASP.NET service in combination with the probably most popular monitoring stack in the cloud native space: Prometheus / Grafana.

TL;DR

You can find the demo project on GitHub. It uses a local Kubernetes cluster (kind) to setup the environment and deploys a demo application that generates some sample metrics. Those metrics are sent to an OTEL collector which serves as a Prometheus metrics endpoint. In the end, the metrics are scraped by Prometheus and displayed in a Grafana dashboard/chart.

Demo Setup

OpenTelemetry – What is it and why should you care?

OpenTelemetry

OpenTelemetry (OTEL) is an open-source CNCF project that aims to provide a vendor-agnostic solution in generating, collecting and handling telemetry data of your infrastructure and services. It is able to receive, process, and export traces, logs, and metrics to different backends like Prometheus , Jaeger or other commercial SaaS offering without the need for your application to have a dependency on those solutions. While OTEL itself doesn’t provide a backend or even analytics capabilities, it serves as the “central monitoring component” and knows how to send the data received to different backends by using so-called “exporters”.

So why should you even care? In today’s world of distributed systems and microservices architectures where developers can release software and services faster and more independently, observability becomes one of the most important features in your environment. Visibility into systems is crucial for the success of your application as it helps you in scaling components, finding bugs and misconfigurations etc.

If you haven’t decided what monitoring or tracing solution you are going to use for your next application, have a look at OpenTelemetry. It gives you the freedom to try out different monitoring solutions or even replace your preferred one later in production.

OpenTelemetry Components

OpenTelemetry currently consists of several components like the cross-language specification (APIs/SDKs and the OpenTelemetry Protocol OTLP) for instrumentation and tools to receive, process/transform and export telemetry data. The SDKs are available in several popular languages like Java, C++, C#, Go etc. You can find the complete list of supported languages here.

Additionally, there is a component called the “OpenTelemetry Collector” which is a vendor-agnostic proxy that receives telemetry data from different sources and can transform that data before sending it to the desired backend solution.

Let’s have a closer look at the components of the collector…receivers, processors and exporters:

  • Receivers – A receiver in OpenTelemetry is the component that is responsible for getting data into a collector. It can be used in a push- or pull-based approach. It can support the OLTP protocol or even scrape a Prometheus /metrics endpoint
  • Processor – Processors are components that let you batch-process, sample, transform and/or enrich your telemetry data that is being received by the collector before handing it over to an exporter. You can add or remove attributes, like for example “personally identifiable information” (PII) or filter data based on regular expressions. A processor is an optional component in a collector pipeline.
  • Exporter – An exporter is responsible for sending data to a backend solution like Prometheus, Azure Monitor, DataDog, Splunk etc.

In the end, it comes down to configuring the collector service with receivers, (optionally) processors and exporters to form a fully functional collector pipeline – official documentation can be found here. The configuration for the demo here is as follows:

receivers:
  otlp:
    protocols:
      http:
      grpc:
processors:
  batch:
exporters:
  logging:
    loglevel: debug
  prometheus:
    endpoint: "0.0.0.0:8889"
service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging, prometheus]

The configuration consists of:

  • one OpenTelemetry Protocol (OTLP) receiver, enabled for http and gRPC communication
  • one processor that is batching the telemetry data with default values (like e.g. a timeout of 200ms)
  • two exporters piping the data to the console (logging) and exposing a Prometheus /metrics endpoint on 0.0.0.0:8889 (remote-write is also possible)

ASP.NET OpenTelemetry

To demonstrate how to send custom metrics from an ASP.NET application to Prometheus via OpenTelemetry, we first need a service that is exposing those metrics. In this demo, we simply create two custom metrics called otel.demo.metric.gauge1 and otel.demo.metric.gauge2 that will be sent to the console (AddConsoleExporter()) and via the OTLP protocol to a collector service (AddOtlpExporter()) that we’ll introduce later on. The application uses the ASP.NET Minimal API and the code is more or less self-explanatory:

using System.Diagnostics.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Metrics;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetryMetrics(metricsProvider =>
{
    metricsProvider
        .AddConsoleExporter()
        .AddOtlpExporter()
        .AddMeter("otel.demo.metric")
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService(serviceName: "otel.demo", serviceVersion: "0.0.1")
        );
});

var app = builder.Build();
var otel_metric = new Meter("otel.demo.metric", "0.0.1");
var randomNum = new Random();
// Create two metrics
var obs_gauge1 = otel_metric.CreateObservableGauge<int>("otel.demo.metric.gauge1", () =>
{
    return randomNum.Next(10, 80);
});
var obs_gauge2 = otel_metric.CreateObservableGauge<double>("otel.demo.metric.gauge2", () =>
{
    return randomNum.NextDouble();
});

app.MapGet("/otelmetric", () =>
{
    return "Hello, Otel-Metric!";
});

app.Run();

We are currently dealing with custom metrics. Of course, ASP.NET also provides out-of-the-box metrics that you can utilize. Just use the ASP.NET instrumentation feature by adding AddAspNetCoreInstrumentation() when configuring the metrics provider – more on that here.

Demo

Time to connect the dots. First, let’s create a Kubernetes cluster using kind where we can publish the demo service, spin-up the OTEL collector instance and run a Prometheus/Grafana environment. If you want to follow along the tutorial, clone the repo from https://github.com/cdennig/otel-demo and switch to the otel-demo directory.

Create a local Kubernetes Cluster

To create a kind cluster that is able to host a Prometheus environment, execute:

$ kind create cluster --name demo-cluster \ 
        --config ./kind/kind-cluster.yaml

The YAML configuration file (./kind/kind-cluster.yaml) adjusts some settings of the Kubernetes control plane so that Prometheus is able to scrape the endpoints of the controller services. Next, create the OpenTelemetry Collector instance.

OTEL Collector

In the manifests directory, you’ll find two Kubernetes manifests. One is containing the configuration for the collector (otel-collector.yaml). It includes the ConfigMap for the collector configuration (which will be mounted as a volume to the collector container), the deployment of the collector itself and a service exposing the ports for data ingestion (4318 for http and 4317 for gRPC) and the metrics endpoint (8889) that will be scraped later on by Prometheus. It looks as follows:

apiVersion: v1

kind: ConfigMap
metadata:
  name: otel-collector-config
data:
  otel-collector-config: |-
    receivers:
      otlp:
        protocols:
          http:
          grpc:
    exporters:
      logging:
        loglevel: debug
      prometheus:
        endpoint: "0.0.0.0:8889"
    processors:
      batch:
    service:
      pipelines:
        metrics:
          receivers: [otlp]
          processors: [batch]
          exporters: [logging, prometheus]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-collector
  labels:
    app: otel-collector
spec:
  replicas: 1
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
        - name: collector
          image: otel/opentelemetry-collector:latest
          args:
            - --config=/etc/otelconf/otel-collector-config.yaml
          ports:
            - name: otel-http
              containerPort: 4318
            - name: otel-grpc
              containerPort: 4317
            - name: prom-metrics
              containerPort: 8889
          volumeMounts:
            - name: otel-config
              mountPath: /etc/otelconf
      volumes:
        - name: otel-config
          configMap:
            name: otel-collector-config
            items:
              - key: otel-collector-config
                path: otel-collector-config.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: otel-collector
  labels:
    app: otel-collector
spec:
  type: ClusterIP
  ports:
    - name: otel-http
      port: 4318
      protocol: TCP
      targetPort: 4318
    - name: otel-grpc
      port: 4317
      protocol: TCP
      targetPort: 4317
    - name: prom-metrics
      port: 8889
      protocol: TCP
      targetPort: prom-metrics
  selector:
    app: otel-collector

Let’s apply the manifest.

$ kubectl apply -f ./manifests/otel-collector.yaml

configmap/otel-collector-config created
deployment.apps/otel-collector created
service/otel-collector created

Check that everything runs as expected:

$ kubectl get pods,deployments,services,endpoints

NAME                                  READY   STATUS    RESTARTS   AGE
pod/otel-collector-5cd54c49b4-gdk9f   1/1     Running   0          5m13s

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/otel-collector   1/1     1            1           5m13s

NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
service/kubernetes       ClusterIP   10.96.0.1      <none>        443/TCP                      22m
service/otel-collector   ClusterIP   10.96.194.28   <none>        4318/TCP,4317/TCP,8889/TCP   5m13s

NAME                       ENDPOINTS                                         AGE
endpoints/kubernetes       172.19.0.9:6443                                   22m
endpoints/otel-collector   10.244.1.2:8889,10.244.1.2:4318,10.244.1.2:4317   5m13s

Now that the OpenTelemetry infrastructure is in place, let’s add the workload exposing the custom metrics.

ASP.NET Workload

The demo application has been containerized and published to the GitHub container registry for your convenience. So to add the workload to your cluster, simply apply the ./manifests/otel-demo-workload.yaml that contains the Deployment manifest and adds two environment variables to configure the OTEL collector endpoint and the OTLP protocol to use – in this case gRPC.

Here’s the relevant part:

spec:
  containers:
  - image: ghcr.io/cdennig/otel-demo:1.0
    name: otel-demo
    env:
    - name: OTEL_EXPORTER_OTLP_ENDPOINT
      value: "http://otel-collector.default.svc.cluster.local:4317"
    - name: OTEL_EXPORTER_OTLP_PROTOCOL
      value: "grpc"

Apply the manifest now.

$ kubectl apply -f ./manifests/otel-demo-workload.yaml

Remember that the application also logs to the console. Let’s query the logs of the ASP.NET service (note that the podname will differ in your environment).

$ kubectl logs po/otel-workload-69cc89d456-9zfs7

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app/
Resource associated with Metric:
    service.name: otel.demo
    service.version: 0.0.1
    service.instance.id: b84c78be-49df-42fa-bd09-0ad13481d826

Export otel.demo.metric.gauge1, Meter: otel.demo.metric/0.0.1
(2022-08-20T11:40:41.4260064Z, 2022-08-20T11:40:51.3451557Z] LongGauge
Value: 10

Export otel.demo.metric.gauge2, Meter: otel.demo.metric/0.0.1
(2022-08-20T11:40:41.4274763Z, 2022-08-20T11:40:51.3451863Z] DoubleGauge
Value: 0.8778815716262417

Export otel.demo.metric.gauge1, Meter: otel.demo.metric/0.0.1
(2022-08-20T11:40:41.4260064Z, 2022-08-20T11:41:01.3387999Z] LongGauge
Value: 19

Export otel.demo.metric.gauge2, Meter: otel.demo.metric/0.0.1
(2022-08-20T11:40:41.4274763Z, 2022-08-20T11:41:01.3388003Z] DoubleGauge
Value: 0.35409627617124295

Also, let’s check if the data will be sent to the collector. Remember it exposes its /metrics endpoint on 0.0.0.0:8889/metrics. Let’s query it by port-forwarding the service to our local machine.

$ kubectl port-forward svc/otel-collector 8889:8889

Forwarding from 127.0.0.1:8889 -> 8889
Forwarding from [::1]:8889 -> 8889

# in a different session, curl the endpoint
$  curl http://localhost:8889/metrics

# HELP otel_demo_metric_gauge1
# TYPE otel_demo_metric_gauge1 gauge
otel_demo_metric_gauge1{instance="b84c78be-49df-42fa-bd09-0ad13481d826",job="otel.demo"} 37
# HELP otel_demo_metric_gauge2
# TYPE otel_demo_metric_gauge2 gauge
otel_demo_metric_gauge2{instance="b84c78be-49df-42fa-bd09-0ad13481d826",job="otel.demo"} 0.45433988869946285

Great, both components – the metric producer and the collector – are working as expected. Now, let’s spin up the Prometheus/Grafana environment, add the service monitor to scrape the /metrics endpoint and create the Grafana dashboard for it.

Add Kube-Prometheus-Stack

Easiest way to add the Prometheus/Grafana stack to your Kubernetes cluster is to use the kube-prometheus-stack Helm chart. We will use a custom values.yaml file to automatically add the static Prometheus target for the OTEL collector called demo/otel-collector (kubeEtc config is only needed in the kind environment):

kubeEtcd:
  service:
    targetPort: 2381
prometheus:
  prometheusSpec:
    additionalScrapeConfigs:
    - job_name: "demo/otel-collector"
      static_configs:
      - targets: ["otel-collector.default.svc.cluster.local:8889"]

Now, add the helm chart to your cluster by executing:

$ helm upgrade --install --wait --timeout 15m \
  --namespace monitoring --create-namespace \
  --repo https://prometheus-community.github.io/helm-charts \
  kube-prometheus-stack kube-prometheus-stack --values ./prom-grafana/values.yaml

Release "kube-prometheus-stack" does not exist. Installing it now.
NAME: kube-prometheus-stack
LAST DEPLOYED: Mon Aug 22 13:53:58 2022
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
NOTES:
kube-prometheus-stack has been installed. Check its status by running:
  kubectl --namespace monitoring get pods -l "release=kube-prometheus-stack"

Let’s have a look at the Prometheus targets, if Prometheus can scrape the OTEL collector endpoint – again, port-forward the service to your local machine and open a browser at http://localhost:9090/targets.

$ kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090
Prometheus targets
Prometheus targets

That looks as expected, now over to Grafana and create a dashboard to display the custom metrics. As done before, port-forward the Grafana service to your local machine and open a browser at http://localhost:3000. Because you need a username/password combination to login to Grafana, we first need to grab that information from a Kubernetes secret:

# Grafana admin username
$ kubectl get secret -n monitoring kube-prometheus-stack-grafana -o jsonpath='{.data.admin-user}' | base64 --decode

# Grafana password
$ kubectl get secret -n monitoring kube-prometheus-stack-grafana -o jsonpath='{.data.admin-password}' | base64 --decode

# port-forward Grafana service
$ kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3000:80

Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

After opening a browser at http://localhost:3000 and a successful login, you should be greeted by the Grafana welcome page.

Grafana Welcome Page

Add a Dashboard for the Custom Metrics

Head to http://localhost:3000/dashboard/import and upload the precreated dashboard from ./prom-grafana/dashboard.json (or simply paste its content to the textbox). After importing the definition, you should be redirected to the dashboard and see our custom metrics being displayed.

Add preconfigured dashboard
OTEL metrics gauge1 and gauge2

Wrap-Up

This demo showed how to use OpenTelemetry custom metrics in an ASP.NET service, sending telemetry data to an OTEL collector instance that is being scraped by a Prometheus instance. To close the loop, those custom metrics are eventually displayed in a Grafana dashboard. The advantage of this solution is that you use a common solution like OpenTelemetry to generate and collect metrics. To which service the data is finally sent and which solution is used to analyze the data can be easily exchanged via OTEL exporter configuration – if you don’t want to use Prometheus, you simply adapt the OTEL pipeline and export the telemetry data to e.g. Azure Monitor or DataDog, Splunk etc.

I hope the demo has given you a good introduction to the world of OpenTelemetry. Happy coding! 🙂

Jürgen Gutsch: ASP.NET Core on .NET 7.0 - Output caching

Finally, Microsoft added output caching to the ASP.NET Core 7.0 preview 6.

Output caching is a middleware that caches the entire output of an endpoint instead of executing the endpoint every time it gets requested. This will make your endpoints a lot faster.

This kind of caching is useful for APIs that provide data that don't change a lot or that gets accessed pretty frequently. It is also useful for more or less static pages, e.g. CMS output, etc. Different caching options will help you to fine-tune your output cache or to vary the cache based on header or query parameter.

For more dynamic pages or APIs that serve data that change a lot, it would make sense to cache more specifically on the data level instead of the entire output.

Trying output caching

To try output caching I created a new empty web app using the .NET CLI:

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

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

In the Program.cs you now need to add output caching to the ServiceCollection as well as using the middleware on the app:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOutputCache();

var app = builder.Build();

app.UseOutputCache();

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

app.Run();

This enables output caching in your application.

Let's use output caching with the classic example that displays the current date and time.

app.MapGet("/time", () => DateTime.Now.ToString());

This creates a new endpoint that displays the current date and time. Every time you refresh the result in the browser, you got a new time displayed. No magic here. Now we are going to add some caching magic to another endpoint:

app.MapGet("/time_cached", () => DateTime.Now.ToString())
	.CacheOutput();

If you access this endpoint and refresh it in the browser, the time will not change. The initial output got cached and you'll receive the cached output every time you refresh the browser.

This is good for more or less static outputs that don't change a lot. What if you have a frequently used API that just needs a short cache to reduce the calculation effort or to just reduce the database access. You can reduce the caching time to, let's say, 10 seconds:

 builder.Services.AddOutputCache(options =>
 {
     options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
 });

This reduces the default cache expiration timespan to 10 seconds.

If you now start refreshing the endpoint we created previously, you'll get a new time every 10 seconds. This means the cache get's released every 10 seconds. Using the options you can also define the size of the cached body or the overall cache size.

If you provide a more dynamic API that receives parameters using query strings. You can vary the cache by the query string:

app.MapGet("/time_refreshable", () => DateTime.Now.ToString())
    .CacheOutput(p => p.VaryByQuery("time"));

This adds another endpoint that varies the cache by the query string argument called "time". This means the query string ?time=now, caches a different result than the query string ?time=later or ?time=before.

The VaryByQuery function allows you to add more than one query string:

app.MapGet("/time_refreshable", () => DateTime.Now.ToString())
    .CacheOutput(p => p.VaryByQuery("time", "culture", "format"));

In case you like to vary the cache by HTTP headers you can do this the same way using the VaryByHeader function:

app.MapGet("/time_cached", () => DateTime.Now.ToString())
    .CacheOutput(p => p.VaryByHeader("content-type"));

Further reading

If you like to explore more complex examples of output caching, it would make sense to have a look into the samples project:

https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs

Code-Inside Blog: How to run a Azure App Service WebJob with parameters

We are using WebJobs in our Azure App Service deployment and they are pretty “easy” for the most part. Just register a WebJobs or deploy your .exe/.bat/.ps1/... under the \site\wwwroot\app_data\Jobs\triggered folder and it should execute as described in the settings.job.

x

If you put any executable in this WebJob folder, it will be executed as planned.

Problem: Parameters

If you have a my-job.exe, then this will be invoked from the runtime. But what if you need to invoke it with a parameter like my-job.exe -param "test"?

Solution: run.cmd

The WebJob environment is “greedy” and will search for a run.cmd (or run.exe) and if this is found, it will be executed and it doesn’t matter if you have any other .exe files there. Stick to the run.cmd and use this to invoke your actual executable like this:

echo "Invoke my-job.exe with parameters - Start"

..\MyJob\my-job.exe -param "test"

echo "Invoke my-job.exe with parameters - Done"

Be aware, that the path must “match”. We use this run.cmd-approach in combination with the is_in_place-option (see here) and are happy with the results).

A more detailed explanation can be found here.

Hope this helps!

Sebastian Seidel: Combining Lottie Animations with Gestures and Scrolling

Matt Goldman revived #XamarinUIJuly and renamed it to #MAUIUIJuly, where each day in July someone from the .NET MAUI community publishes a blog post or video showing some incredible UI magic in MAUI. In this contribution I will show you how to combine Lottie animations with gestures and scrollable containers to spice up your .NET MAUI App UI!

Christina Hirth : DDD Europe 2022 Watch-List

I attend conferences and open spaces for more than 15 years but I can’t remember ever being keener to go to a conference than the DDD EU this year. But I still haven’t imagined that my list for “watch later”-videos will be almost as long as the number of talks – including the ones from the DDD Foundations (2 pre-conference days).

I was so full of expectations because I would be a speaker at an international conference for the first time and the opportunity to meet all those wonderful people who became friends in the last two years! (I won’t even try to list the names because I would surely miss a few). The most often repeated sentence on those five days wasn’t “Can you see my screen?” anymore but “Do you know that we never met before IRL?!” 🤗

This was only one of those great evenings meeting old friends and making new ones 🙂 (After two years of collaboration, the Virtual DDD organizers have finally met too!)

But now back to the lists:

Talks I haven’t seen but I should:

  1. DDD Foundations with clever people and interesting talks which should/could land in our ddd-crew repositories. (In general, the sessions are not too long, I will probably browse through all of them.)
  2. Main Conference

Talks to revisit

This list is not the list of “good talks”; I can’t remember being at any talk I wished I wouldn’t. But these here need to be seen and listened to more than once (at least I do).

Domain-Driven Design in ProductLand – Alberto Brandolini

Alberto speaking the truth about product development is exactly my kind of radical candour.

Independent Service Heuristics: a rapid, business-friendly approach to flow-oriented boundaries – Matthew Skelton and Nick Tune

The tweet tells it all: an essential new method in our toolbox

The Fractal Geometry of Software Design – Vladik Khononov

Mindblowing. I will probably have to re-watch this video a couple of times until I get my brain around all of the facets Valdik touches in his talk.

Sociotechnical Systems Design for the “Digital Coal Mines” – Trond Hjorteland

This talk is not something I haven’t understood – I understand it completely. I will still re-watch it because it contains historical and actual arguments and requirements for employers on how they have to re-think their organizational models.

This is the longest list of videos I have ever bookmarked (and published as a suggestion for you all). Still, it is how it is: the DDD-Eu 2022 was, in my opinion, the most mature conference I ever participated.

At the same time, there is always time for jokes when Mathias Verraes and Nick Tune are around (and we are around them, of course) 😃

Stefan Henneken: IEC 61131-3: SOLID – Das Liskov Substitution Principle

„Das Liskov Substitution Principle (LSP) fordert, dass abgeleitete FBs immer zu ihren Basis-FB kompatibel sind. Abgeleitete FBs müssen sich so verhalten wie ihr jeweiliger Basis-FB. Ein abgeleiteter FB darf den Basis-FB erweitern, aber nicht einschränken.“ Dieses ist die Kernaussage des Liskov Substitution Principle (LSP), welches Barbara Liskov schon Ende der 1980iger Jahre formulierte. Obwohl das Liskov Substitution Principle (LSP) eines der einfacheren SOLID-Prinzipien ist, tritt deren Verletzung doch sehr häufig auf. Warum das Liskov Substitution Principle (LSP) wichtig ist, zeigt das folgende Beispiel.

Ausgangssituation

Erneut wird das Beispiel verwendet, welches zuvor in den beiden vorherigen Posts entwickelt und optimiert wurde. Kern des Beispiels sind drei Lampentypen, welche durch die Funktionsblöcke FB_LampOnOff, FB_LampSetDirect und FB_LampUpDown abgebildet werden. Die Schnittstelle I_Lamp und der abstrakte Funktionsblock FB_Lamp gewährleisten eine saubere Entkopplung zwischen den jeweiligen Lampentypen und dem übergeordneten Controller FB_Controller.

FB_Controller greift nicht mehr auf konkrete Instanzen, sondern nur noch auf eine Referenz des abstrakten Funktionsblock FB_Lamp zu. Für das Auflösen der festen Koppelung wird das IEC 61131-3: SOLID – Das Dependency Inversion Principle (DIP) angewendet.

Zur Realisierung der geforderten Funktionsweise, stellt jeder Lampentyp seine eigenen Methoden bereit. Aus diesem Grund besitzt jeder Lampentyp einen entsprechenden Adapter-Funktionsblock (FB_LampOnOffAdapter, FB_LampSetDirectAdapter und FB_LampUpDownAdapter), der für das Mapping zwischen der abstrakten Lampe (FB_Lamp) und den konkreten Lampentypen (FB_LampOnOff, FB_LampSetDirect und FB_LampUpDown) zuständig ist. Unterstützt wird diese Optimierung durch das IEC 61131-3: SOLID – Das Single Responsibility Principle (SRP).

Erweiterung der Implementierung

Die drei geforderten Lampentypen lassen sich durch das bisherige Software-Design gut abbilden. Trotzdem kann es passieren, dass Erweiterungen, die auf dem ersten Blick einfach wirken, später zu Schwierigkeiten führen. Als Beispiel soll hier der neue Lampentyp FB_LampSetDirectDALI dienen.

DALI steht für Digital Addressable Lighting Interface und ist ein Protokoll zur Ansteuerung von lichttechnischen Geräten. Grundsätzlich verhält sich der neue Baustein wie FB_LampSetDirect, allerdings wird der Ausgangswert bei DALI nicht in 0-100 %, sondern in 0-254 angegeben.

Optimierung und Analyse der Erweiterungen

Welche Ansätze stehen zur Verfügung, um diese Erweiterung umzusetzen? Dabei sollen auch die unterschiedlichen Ansätze genauer analysiert werden.

Ansatz 1: Quick & Dirty

Hoher Zeitdruck kann dazu verleiten die Umsetzung Quick & Dirty zu realisieren. Da FB_LampSetDirect sich ähnlich verhält wie der neue DALI-Lampentyp, erbt FB_LampSetDirectDALI von FB_LampSetDirect. Um den Wertebereich von 0-254 zu ermöglichen, wird die Methode SetLightLevel() von FB_LampSetDirectDALI überschrieben.

METHOD PUBLIC SetLightLevel
VAR_INPUT
  nNewLightLevel    : BYTE(0..254);
END_VAR
nLightLevel := nNewLightLevel;

Auch der neue Adapter-Funktionsblock (FB_LampSetDirectDALIAdapter) wird so angepasst, dass die Methoden den Wertebereich von 0-254 berücksichtigen.

Als Beispiel sollen hier die Methoden DimUp() und On() gezeigt werden:

METHOD PUBLIC DimUp
IF (fbLampSetDirectDALI.nLightLevel <= 249) THEN
  fbLampSetDirectDALI.SetLightLevel(fbLampSetDirectDALI.nLightLevel + 5);
END_IF
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(fbLampSetDirectDALI.nLightLevel);
END_IF
METHOD PUBLIC On
fbLampSetDirectDALI.SetLightLevel(254);
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(fbLampSetDirectDALI.nLightLevel);
END_IF

Das vereinfachte UML-Diagramm zeigt die Integration der Funktionsblöcke für die DALI-Lampe in das bestehende Software-Design:

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 1 (TwinCAT 3.1.4024) auf GitHub

Dieser Ansatz setzt die Forderungen durch eine pragmatische Herangehensweise schnell und einfach um. Doch dadurch wurden auch einige Besonderheiten hinzugefügt, welche den Einsatz der Bausteine in einer Applikation erschweren.

Wie soll sich z.B. eine Bedieneroberfläche verhalten, wenn sich diese auf eine Instanz von FB_Controller verbindet und FB_AnalogValue einen Wert von 100 ausgibt? Bedeutet 100, dass die aktuelle Lampe auf 100 % steht, oder gibt die neue DALI-Lampe einen Wert von 100 aus, was deutlich unter 100 % liegen würde?

Der Anwender von FB_Controller muss immer den aktiven Lampentyp kennen, um den aktuellen Ausgangswert korrekt interpretieren zu können. FB_LampSetDirectDALI erbt zwar von FB_LampSetDirect, verändert aber dessen Verhalten. In diesem Beispiel durch das Überschreiben der Methode SetLightLevel(). Der abgeleitete FB (FB_LampSetDirectDALI) verhält sich anders als der Basis-FB (FB_LampSetDirect). FB_LampSetDirect kann nicht mehr durch FB_LampSetDirectDALI ersetzt (substituiert) werden. Das Liskov Substitution Principle (LSP) wird verletzt.

Ansatz 2: Optionalität

Bei diesem Ansatz enthält jeder Lampentyp eine Eigenschaft, die Auskunft über die genaue Funktionsweise des Funktionsblock zurückgibt.

In .NET wird z.B. dieser Ansatz in der abstrakten Klasse System.IO.Stream verwendet. Die Klasse Stream dient als Basisklasse für spezialisierte Streams (z.B. FileStream und NetworkStream) und legt die wichtigsten Methoden und Eigenschaften fest. Hierzu gehören auch die Methoden Write(), Read() und Seek(). Da nicht jeder Stream alle Funktionen zur Verfügung stellen kann, geben die Eigenschaften CanRead, CanWrite und CanSeek Auskunft darüber, ob die entsprechende Methode vom jeweiligen Stream unterstützt wird. So kann bei NetworkStream zur Laufzeit geprüft werden, ob ein Schreiben in den Stream möglich ist, oder ob es sich um einen read-only Stream handelt.

Bei unserem Beispiel wird I_Lamp durch die Eigenschaft bIsDALIDevice erweitert.

Dadurch erhält auch FB_Lamp und somit jeder Adapter-Funktionsblock diese Eigenschaft. Da die Funktionalität von bIsDALIDevice in allen Adapter-Funktionsblöcken gleich ist, wird bIsDALIDevice in FB_Lamp nicht als abstract deklariert. Dadurch ist es nicht notwendig, dass alle Adapter-Funktionsblöcke diese Eigenschaft selbst implementieren müssen. Die Funktionalität von bIsDALIDevice wird von FB_Lamp an alle Adapter-Funktionsblöcke vererbt.

Für FB_LampSetDirectDALIAdapter wird in der Methode FB_init() die Backing-Variable der Eigenschaft bIsDALIDevice auf TRUE gesetzt.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains  : BOOL;
  bInCopyCode   : BOOL;
END_VAR
SUPER^._bIsDALIDevice := TRUE;

Bei allen anderen Adapter-Funktionsblöcken behält _bIsDALIDevice seinen Initialisierungswert (FALSE). Der Einsatz der Methode FB_init() ist bei diesen Adapter-Funktionsblöcken nicht notwendig.

Der Anwender von FB_Controller (Baustein MAIN) kann jetzt zur Laufzeit des Programms abfragen, ob die aktuelle Lampe eine DALI-Lampe ist oder nicht. Ist dieses der Fall, wird der Ausgangswert entsprechend auf 0-100 % skaliert.

IF (__ISVALIDREF(fbController.refActiveLamp) AND_THEN fbController.refActiveLamp.bIsDALIDevice) THEN
  nLightLevel := TO_BYTE(fbController.fbActualValue.nValue * 100.0 / 254.0);
ELSE
  nLightLevel := fbController.fbActualValue.nValue;
END_IF

Anmerkung: Wichtig ist hierbei die Verwendung des Operators AND_THEN statt THEN. Hierdurch wird der Ausdruck rechts von AND_THEN nur dann ausgeführt, wenn der erste Operand (links von AND_THEN) TRUE ist. Das ist hierbei wichtig, da sonst bei einer ungültigen Referenz auf die aktive Lampe (refActiveLamp) der Ausdruck fbController.refActiveLamp.bIsDALIDevice die Ausführung des Programms beenden würde.

Im UML-Diagramm ist zu erkennen wie FB_Lamp über die Schnittstelle I_Lamp die Eigenschaft bIsDALIDevice erhält und somit von allen Adapter-Funktionsblöcken geerbt wird:

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 2 (TwinCAT 3.1.4024) auf GitHub

Auch bei diesem Ansatz wird das Liskov Substitution Principle (LSP) weiterhin verletzt. FB_LampSetDirectDALI verhält sich nach wie vor unterschiedlich zu FB_LampSetDirect. Diese Unterschiedlichkeit muss vom Anwender berücksichtigt (Abfragen von bIsDALIDevice) und korrigiert (Skalierung auf 0-100 %) werden. Dieses wird schnell übersehen oder fehlerhaft umgesetzt.

Ansatz 3: Harmonisierung

Um das Liskov Substitution Principle (LSP) nicht weiter zu verletzen, wird die Vererbung zwischen FB_LampSetDirect und FB_LampSetDirectDALI aufgelöst. Auch wenn beide Funktionsblöcke auf dem ersten Blick sehr ähnlich wirken, so sollte an dieser Stelle auf die Vererbung verzichtet werden.

Die Adapter-Funktionsblöcke stellen sicher, dass alle Lampentypen mit den gleichen Methoden steuerbar sind. Unterschiede gibt es allerdings weiterhin bei der Darstellung des Ausgangswertes.

In FB_Controller wird der Ausgangswert der aktiven Lampe durch eine Instanz von FB_AnalogValue dargestellt. Übermittelt wird ein neuer Ausgangswert durch die Methode Update(). Damit auch der Ausgangswert einheitlich dargestellt wird, wird vor dem Aufruf der Methode Update() dieser auf 0-100 % skaliert. Die notwendigen Anpassungen erfolgen ausschließlich in den Methoden DimDown(), DimUp(), Off() und On() von FB_LampSetDirectDALIAdapter.

Als Beispiel soll hier die Methode On() gezeigt werden:

METHOD PUBLIC On
fbLampSetDirectDALI.SetLightLevel(254);
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(TO_BYTE(fbLampSetDirectDALI.nLightLevel * 100.0 / 254.0));
END_IF

Der Adapter-Funktionsblock enthält alle notwendigen Anweisungen, wodurch sich die DALI-Lampe nach Außen so verhält wie erwartet. FB_LampSetDirectDALI bleibt bei diesem Lösungsansatz unverändert.

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 3 (TwinCAT 3.1.4024) auf GitHub

Analyse der Optimierung

Durch verschiedene Techniken ist es uns möglich, die gewünschte Erweiterung zu implementieren, ohne dass das Liskov Substitution Principle (LSP) verletzt wird. Voraussetzung, um das LSP zu verletzen, ist Vererbung. Wird das LSP verletzt, so ist dieses evtl. ein Hinweis auf eine schlechte Vererbungshierarchie innerhalb des Software-Designs.

Warum ist es wichtig, dass Liskov Substitution Principle (LSP) einzuhalten? Funktionsblöcke können auch als Parameter übergeben werden. Würde ein POU einen Parameter vom Typ FB_LampSetDirect erwarten, so könnte, bei der Verwendung von Vererbung, auch FB_LampSetDirectDALI übergeben werden. Die Arbeitsweise der Methode SetLightLevel() ist aber bei beiden Funktionsblöcken unterschiedlich. Solche Unterschiede können zu unerwünschten Verhalten innerhalb einer Anlage führen.

Die Definition des Liskov Substitution Principle

Sei q(x) eine beweisbare Eigenschaft von Objekten x des Typs T. Dann soll q(y) für Objekte y des Typs S wahr sein, wobei S ein Untertyp von T ist.

So lautet, etwas formeller ausgedrückt, die Definition des Liskov Substitution Principle (LSP) von Barbara Liskov. Wie weiter oben schon erwähnt, wurde schon Ende der 1980iger Jahre dieses Prinzip definiert. Die vollständige Ausarbeitung hierzu wurde unter dem Titel Data Abstraction and Hierarchy veröffentlicht.

Barbara Liskov promovierte 1968 als eine der ersten Frauen in Informatik. 2008 erhielt sie, ebenfalls als eine der ersten Frauen, den Turing Award. Schon früh beschäftigte sie sich mit der objektorientierten Programmierung und somit auch mit der Vererbung von Klassen (Funktionsblöcken).

Die Vererbung stellt zwei Funktionsblöcke in eine bestimmte Beziehung zueinander. Vererbung beschreibt hierbei eine istein-Beziehung. Erbt FB_LampSetDirectDALI von FB_LampSetDirect, so ist die DALI-Lampe eine (normale) Lampe, erweitert um besondere (zusätzliche) Funktionen. Überall wo FB_LampSetDirect verwendet wird, könnte auch FB_LampSetDirectDALI zum Einsatz kommen. FB_LampSetDirect kann durch FB_LampSetDirectDALI substituiert werden. Ist dieses nicht sichergestellt, so sollte die Vererbung an dieser Stelle hinterfragt werden.

Robert C. Martin hat dieses Prinzip mit in die SOLID-Prinzipien aufgenommen. In dem Buch (Amazon-Werbelink *) Clean Architecture: Das Praxis-Handbuch für professionelles Softwaredesign wird dieses Prinzip weiter erläutert und auf den Bereich der Softwarearchitektur ausgedehnt.

Zusammenfassung

Durch die Erweiterung des obigen Beispiels, haben Sie das Liskov Substitution Principle (LSP) kennen gelernt. Gerade komplexe Vererbungshierarchien sind anfällig für die Verletzung dieses Prinzips. Obwohl sich die formelle Definition des Liskov Substitution Principle (LSP) kompliziert anhört, so ist die Kernaussage dieses Prinzips doch einfach zu verstehen.

Im nächsten Post soll unserer Beispiel erneut erweitert werden. Dabei wird das Interface Segregation Principle (ISP) eine zentrale Rolle haben.

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" -&gt; null
      - location = "switzerlandnorth" -&gt; null
      - name     = "terrfaform-playground" -&gt; null
      - tags     = {} -&gt; null
    }

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

      - blob_properties {
          - change_feed_enabled      = false -&gt; null
          - last_access_time_enabled = false -&gt; null
          - versioning_enabled       = false -&gt; null
        }

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

      - queue_properties {

          - hour_metrics {
              - enabled               = true -&gt; null
              - include_apis          = true -&gt; null
              - retention_policy_days = 7 -&gt; null
              - version               = "1.0" -&gt; null
            }

          - logging {
              - delete                = false -&gt; null
              - read                  = false -&gt; null
              - retention_policy_days = 0 -&gt; null
              - version               = "1.0" -&gt; null
              - write                 = false -&gt; null
            }

          - minute_metrics {
              - enabled               = false -&gt; null
              - include_apis          = false -&gt; null
              - retention_policy_days = 0 -&gt; null
              - version               = "1.0" -&gt; null
            }
        }

      - share_properties {

          - retention_policy {
              - days = 7 -&gt; 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 = "~&gt; 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 "~&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!

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 = &lt;&lt;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.

Starting situation

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:

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

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. ;-)

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