tia-csharp-common — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited tia-csharp-common (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Source: TIA Portal Openness General Functions Manual (03/2026, V21); V21 IntelliSense XML documentation files (PublicAPI\V21\net48\*.xml)
V21 splits the API across multiple DLLs instead of a single Siemens.Engineering.dll. Every project needs Siemens.Engineering.Base.dll; add domain DLLs as required. Referencing the wrong DLL (or omitting one) causes TypeLoadException at runtime.
Full mapping: See references/assembly-namespace-map.md for the V21 namespace → DLL → domain skill table, cross-assembly warnings, and csproj patterns.Only import what is actually used. Commented using blocks for each assembly (Base, Step7, WinCC, Startdrive, Safety) are in references/assembly-namespace-map.md under "Common using blocks by assembly".
Not needed for Add-Ins. TIA Portal loads Add-In assemblies via its own loader. This section applies only to standalone console/desktop Openness applications.
HKLM\SOFTWARE\Siemens\Automation\InstalledApps\Totally Integrated Automation Portal V21 → value INSTALLPATHC:\Program Files\Siemens\Automation\Portal V21\PublicAPI\V21\net48\C:\Program Files\Siemens\Automation\Portal V21\PublicAPI\V21\Schemas\When using the AssemblyResolve approach, register the event before any Openness type is referenced. This includes method parameters, return types, and class properties — not just method bodies. A correctly configured app.config binding is an alternative.
Recommended pattern: register in a static constructor of Program, then move all Openness code to a separate class so the resolver is active before those classes are loaded.
internal static class Program
{
// Register resolver at earliest possible point
static Program()
{
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
}
public static void Main()
{
// All Openness code must be in a separate class
var app = new OpennessApp();
app.Run();
}
private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
{
// Read install path from registry (recommended) or hardcode for simple tools
string opennessFolder = GetOpennessInstallPath(); // implement per project
AssemblyName requestedName = new AssemblyName(args.Name);
string filePath = Path.Combine(opennessFolder,
string.Concat(requestedName.Name, ".dll"));
if (!requestedName.Name.StartsWith("Siemens.Engineering.") ||
!File.Exists(filePath))
return null;
Assembly loaded = Assembly.LoadFrom(filePath);
if (requestedName.FullName != loaded.GetName().FullName)
throw new FileNotFoundException(
"TIA Portal Openness version does not match", filePath);
return loaded;
}
}Key rules:
Copy Local: False (<Private>False</Private>) must be set for every Siemens.Engineering assembly reference.Base.dll is always needed; add Step7.dll, WinCC.dll,Startdrive.dll, etc. per task. The resolver handles all of them automatically because it matches any name starting with Siemens.Engineering..
HKLM\SOFTWARE\Siemens\Automation\InstalledApps\Totally Integrated Automation Portal V21 (value INSTALLPATH) rather than hardcoding it.
Registry-based path discovery:
private static string GetOpennessInstallPath()
{
using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\Siemens\Automation\InstalledApps\Totally Integrated Automation Portal V21"))
{
string installPath = key?.GetValue("INSTALLPATH") as string;
if (string.IsNullOrEmpty(installPath))
throw new FileNotFoundException("TIA Portal V21 installation not found in registry.");
return Path.Combine(installPath, @"PublicAPI\V21\net48");
}
}Alternative — app.config: For stable installations where the path is known at build time, an app.config file with <assemblyBinding> / <codeBase> entries is a simpler option that requires no code. Use the AssemblyResolve event for tools that need to discover the install path at runtime.
Always use a using statement so the session is disposed on exit or exception:
internal class OpennessApp
{
public void Run()
{
using (TiaPortal tiaPortal = new TiaPortal(TiaPortalMode.WithUserInterface))
{
// All Openness work goes here
}
// TIA Portal is disposed (not necessarily closed) after this point
}
}Modes:
TiaPortalMode.WithUserInterface — starts a new TIA Portal instance with a visible GUITiaPortalMode.WithoutUserInterface — starts a new headless instance, suitable for automation pipelinesAttaching to a running instance:
// Attach to the first running TIA Portal process of this Openness version
TiaPortal tiaPortal = TiaPortal.GetProcesses().First().Attach();Use GetProcesses() when TIA Portal is already open and you do not want to start a new instance. Returns only processes from the same Openness version as the loaded assembly.
Dispose vs. close:
Dispose() closes TIA Portal.Dispose() only disconnects.process is closed externally throws NonRecoverableException.
Full patterns: references/portal-services-and-events.md (Add-In MessageBoxProvider, ProgressProvider, FeedbackProvider via GetService<T>(), and the Disposed, Notification, Confirmation, Authentication events on TiaPortal).
Rules that always apply:
GetService<T>() returns null in Add-In status callbacks (OnUpdateStatus);use services only in action callbacks.
TiaPortal event handler in a finally block.Confirmation, e.Result must be "Yes", "YesToAll" or "No"; any othervalue throws.
Authentication (V17+) fires only for UMAC-protected projects; sete.AuthenticationTypeProvider (DesktopSso, Anonymous, Interactive, Credentials).
Highly recommended for all non-trivial operations. Signals TIA Portal that a controlled activity is in progress and displays a dialog to the user.
using (ExclusiveAccess exclusiveAccess = tiaPortal.ExclusiveAccess("Generating project"))
{
// All Openness operations here run under exclusive access
// Optionally update the displayed message during long operations
exclusiveAccess.Text = "Compiling software";
// ...
exclusiveAccess.Text = "Downloading to PLC";
}
// Exclusive access is released on disposeRules:
ExclusiveAccess can exist at a time — a second attempt throws a recoverableexception while the first is still active.
exclusiveAccess.Text = string.Empty or null clears the displayed message.Group multiple modifications into a single undo unit. Requires an active ExclusiveAccess.
using (ExclusiveAccess exclusiveAccess = tiaPortal.ExclusiveAccess("Bulk edit"))
{
using (Transaction transaction = exclusiveAccess.Transaction(project, "Create blocks"))
{
// Perform all modifications
project.DeviceGroups.Create("Line_1");
project.DeviceGroups.Create("Line_2");
// MANDATORY: call CommitOnDispose() to persist changes
// If this line is never reached, the transaction is rolled back
transaction.CommitOnDispose();
}
}Rollback rules — critical:
CommitOnDispose() is never called → rolled back on dispose.including when CommitOnDispose() was already requested or the exception was caught.
EngineeringTargetInvocationException, a later CommitOnDispose() request canitself raise a recoverable exception; check CanCommit and do not assume persistence.
Not allowed inside a transaction: Compile, go online/offline, ProjectText import/export, opening or closing a global library, project create/open/open-with-upgrade/save/save-as/close, and changing a TIA Portal setting. These calls raise a recoverable exception while a transaction is active.
These rules are mandatory for generated C# Openness code:
ExclusiveAccess scope and a Transaction when the API permits transactions.
using blocks for ExclusiveAccess, Transaction, TiaPortal, and other disposableOpenness objects so rollback and release behavior is explicit. Close projects explicitly according to lifecycle ownership; Project is not an IDisposable object.
transaction.CommitOnDispose() only after every validation and mutation inthe transaction has succeeded.
changes. Do not present generated project changes as deployable until that check passes.
use the narrowest possible ExclusiveAccess scope, and require a preview or user confirmation before the destructive call.
Two top-level categories:
| Category | Base type | Behaviour |
|---|---|---|
| Recoverable | Siemens.Engineering.EngineeringException | API call failed; session remains valid |
| Non-recoverable | Siemens.Engineering.NonRecoverableException | TIA Portal closed; restart required |
Recoverable subtypes:
| Type | Typical cause |
|---|---|
EngineeringSecurityException | Missing access rights |
EngineeringObjectDisposedException | Object no longer exists (e.g. deleted) |
EngineeringNotSupportedException | Attribute or operation not available on this object |
EngineeringTargetInvocationException | General failure despite a valid API call |
EngineeringRuntimeException | Runtime error, e.g. invalid cast |
EngineeringOutOfMemoryException | Insufficient resources in TIA Portal instance |
EngineeringUserAbortException | Operation cancelled by the user (e.g. import dialog) |
EngineeringDelegateInvocationException | Exception thrown inside a caller-supplied delegate |
MissingProductsException | Project requires TIA Portal products/packages not installed (V18+) |
Recommended catch order:
try
{
// Openness work
}
catch (EngineeringSecurityException ex)
{
Console.WriteLine($"Access denied: {ex.Message}");
}
catch (EngineeringObjectDisposedException ex)
{
Console.WriteLine($"Object disposed: {ex.Message}");
}
catch (EngineeringNotSupportedException ex)
{
Console.WriteLine($"Not supported: {ex.MessageData.Text}");
foreach (ExceptionMessageData detail in ex.DetailMessageData)
Console.WriteLine($" Detail: {detail.Text}");
}
catch (MissingProductsException ex)
{
Console.WriteLine($"Missing products: {ex.Message}");
}
catch (EngineeringTargetInvocationException)
{
throw; // Do not swallow — rethrow for caller to handle
}
catch (EngineeringException)
{
throw; // Do not swallow general engineering exceptions
}
catch (NonRecoverableException ex)
{
Console.WriteLine($"Fatal — TIA Portal closed: {ex.Message}");
// Must restart TIA Portal; the session is gone
}NonRecoverableException is generic; it can also mean TIA Portal crashed inside the call. Never retry the same call or continue an unattended batch after it. The real cause is in the Portal's crash report: %ProgramData%\Siemens\Automation\Portal V21\Diagnostics\<build>\<id>\<id>.dr (a ZIP with ErrorReport.xml, Reason="Openness") and the Windows Application log at the crash time.
When opening multiple projects or working in multiuser environments:
// Primary — visible in project navigation, full read-write access
Project primary = tiaPortal.Projects.Open(
new FileInfo(path), umacDelegate, ProjectOpenMode.Primary);
// Secondary — hidden from UI, always read-only regardless of user rights
Project secondary = tiaPortal.Projects.Open(
new FileInfo(path), null, ProjectOpenMode.Secondary);
bool isPrimary = secondary.IsPrimary; // falseRules:
opening it as secondary.
Most engineering objects are accessed through compositions. Available methods:
| Method | Behaviour |
|---|---|
Create(id, ...) | Creates and adds a new instance. Signature varies per composition. |
Find(id) | Finds by identifier. Non-recursive — searches current level only. |
GetEnumerator() | Used implicitly in foreach loops. |
Contains(obj) | Returns bool — checks if object is in the composition. |
IndexOf(obj) | Returns int index of the object in the composition. |
Import(path, ImportOptions) | Available on compositions that support import. ImportOptions: None or Overwrite. |
Delete(obj) | Removes and deletes the instance. |
Not all methods are available on every composition — check domain skills.
// Reference equality (same .NET object reference)
bool sameRef = (objA == objB);
// TIA Portal identity equality (same engineering object, possibly different references)
bool sameObj = System.Object.Equals(objA, objB);Use System.Object.Equals() when comparing objects retrieved through different navigation paths that may represent the same underlying engineering object.
All DirectoryInfo and FileInfo instances passed to Openness must use absolute paths. Relative paths cause an exception at runtime.
// Correct
var dir = new DirectoryInfo(@"C:\Projects\MyProject\Export");
// Wrong — will throw
var dir = new DirectoryInfo(@"Export");Useful for dynamic tooling or when setting multiple attributes efficiently on HW objects.
// Get names of all available attributes
IList<EngineeringAttributeInfo> infos =
((IEngineeringObject)deviceItem).GetAttributeInfos();
// Read all readable attributes in one call
IList<string> names = infos.Select(i => i.Name).ToList();
IList<object> values = ((IEngineeringObject)deviceItem).GetAttributes(names);// AttributeAccessOptions: None, ReadOnly, WriteOnly, ReadWrite
IReadOnlyList<KeyValuePair<string, object>> readableAttrs =
deviceItem.GetAttributes(AttributeAccessOptions.ReadOnly);var pairs = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("Name", "NewModuleName"),
new KeyValuePair<string, object>("Comment", "Auto-generated")
};
// Overload with callback fires after each attribute is set
deviceItem.SetAttributes(pairs, (name, val, ex) =>
{
if (ex != null)
Console.WriteLine($"Failed to set {name}: {ex.Message}");
});
// Overload without callback — for non-HW objects
((IEngineeringObject)someObject).SetAttributes(pairs);~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.