Serilog.Sinks.MicrosoftTeams.Alternative is a Serilog sink that posts log events to a Microsoft
Teams channel over an incoming webhook or a Power Automate workflow URL. It is published as the
NuGet package Serilog.Sinks.MicrosoftTeams.Alternative (GeneratePackageOnBuild is on, so every
build produces a .nupkg and a .snupkg). The project is a maintained fork of
DixonDs/serilog-sinks-teams.
One solution src/Serilog.Sinks.MicrosoftTeams.Alternative.sln with exactly two projects:
src/Serilog.Sinks.MicrosoftTeams.Alternative/Serilog.Sinks.MicrosoftTeams.Alternative.csproj, the library, multi-targetingnet8.0;net10.0.src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests/Serilog.Sinks.MicrosoftTeams.Alternative.Tests.csproj, MSTest plus WireMock.Net, single targetnet10.0,IsPackable=false.
Layout inside src/Serilog.Sinks.MicrosoftTeams.Alternative:
LoggerConfigurationMicrosoftTeamsExtensions.cs: the public entry point, namespaceSerilog, twoWriteTo.MicrosoftTeams(...)overloads. One takes the individual parameters and builds aMicrosoftTeamsSinkOptions, the other takes that options object. Both end in the same line that wrapsMicrosoftTeamsSinkinto aPeriodicBatchingSink.Sinks/MicrosoftTeams/Alternative/MicrosoftTeamsSink.cs: the sink itself, anIBatchedLogEventSink.EmitBatchAsyncgroups the events withGetMessagesToSendand posts them withPostMessages. The private helpersCreateMessageCard,CreateMessage,GetChannelUri,GetRenderedMessage,GetRenderedTitleandGetFactsdo one thing each, keep new logic in that shape.Sinks/MicrosoftTeams/Alternative/MicrosoftTeamsSinkOptions.cs: every configuration value, all get-only, validated in the constructor.MicrosoftTeamsSinkChannelHandlerOptions.csholds the multi channel configuration,MicrosoftTeamsSinkOptionsButton.csa single URL button,MicrosoftExtendedLogEvent.csa log event plus its first and last occurrence.Sinks/MicrosoftTeams/Alternative/Core/: the JSON payload types.MicrosoftTeamsMessageCardand itsSection/Fact/Actiontypes are the legacy MessageCard format,MicrosoftTeamsMessageandMicrosoftTeamsAttachmentare the adaptive card format for Power Automate workflows.Sinks/MicrosoftTeams/Alternative/Constants/,Enumerations/,Exceptions/,Extensions/: one type each (SinkConstants,MicrosoftTeamsColors,LoggingException,EnumerableExtensions).GlobalUsings.cs: all usings of the project.
Layout inside src/Serilog.Sinks.MicrosoftTeams.Alternative.Tests:
MicrosoftTeamsSinkTest.cs: the only test class. It logs through a realLoggerConfigurationagainst a WireMock server and asserts on the requests that arrive there.TestHelper.cs: theCreateLogger*factories and the two mock server factories.Extensions/WireMockExtensions.cs:AddDefaultChannelandAddChannel, the request matchers the assertions rely on.appsettings.TwoChannelsExample.jsonandTestException.txt: copied to the output directory withCopyToOutputDirectory=Always.GlobalUsings.cs: all usings of the test project.
Repository root: README.md (badges, target frameworks, contributor table), HowToUse.md (the
actual user documentation with the option table), Changelog.md, Updating.md (the five step
release checklist), License.txt (MIT), Icon.png (the package icon),
BuildAndPushPackage.bat, Delete-BIN-OBJ-Folders.bat and .all-contributorsrc. There is no
.github folder and no pipeline file, the repository has no CI.
src/Directory.Build.props exists but sets exactly one property, GenerateDocumentationFile.
Everything else lives directly in the two .csproj files and is duplicated there.
dotnet build src/Serilog.Sinks.MicrosoftTeams.Alternative.slndotnet test src/Serilog.Sinks.MicrosoftTeams.Alternative.sln- Restore needs nuget.org and this machine has a private feed configured globally. That feed
answers with a connection refused, which fails restore with
NU1301and, becauseNuGetAuditis on, additionally withNU1900. Build and test with an explicit source then:dotnet build src/Serilog.Sinks.MicrosoftTeams.Alternative.sln --source https://api.nuget.org/v3/index.json.dotnet list package --outdateddoes not forward--sourceto its implicit restore, use--no-restorethere after a successful build. TreatWarningsAsErrorsis enabled in both projects, so every warning breaks the build, NuGet warnings (NU****) from restore included. A clean build reports zero warnings, keep it that way.NU1803(HTTP source usage during restore) is the one warning suppressed viaNoWarn. Fix warnings instead of extending that list.NuGetAuditandNuGetAuditMode=allare on, so a vulnerable transitive package fails the build too.GenerateDocumentationFilefromsrc/Directory.Build.propsplusTreatWarningsAsErrorsmeans a missing XML doc comment (CS1591) is a build error, not a hint.- Versions come from GitVersion.MsBuild out of the git tags, for example
1.5.1-1for the first commit after tag1.5.0. Never edit a version property or an assembly version by hand. - The library multi-targets, so
dotnet buildproducesbin/Release/net8.0andbin/Release/net10.0plus the package next to them inbin/Release. - Tests are MSTest with WireMock.Net, in the single test project.
dotnet testruns 13 tests in about three seconds. They need no internet, but they do need the local TCP port 63210, and they only pass when the two webhook environment variables are unset, see "Known quirks". Never claim a test run happened without running it. - A behaviour change that the WireMock tests cannot see (the actual card rendering in Teams) is
verified by setting
MicrosoftTeamsWebhookUrlorMicrosoftTeamsWebhookUrlPowerAutomateand looking at the channel. Read the warning about those variables under "Known quirks" first.
Follow the surrounding code, it is consistent throughout every file:
- File header comment block with
<copyright file="..." company="SeppPenner and the Serilog contributors">and a<summary>, then the file-scoped namespace. - XML doc comments on every type and every member, private members included, no exceptions.
Implementations of an interface member additionally carry
<inheritdoc cref="..."/>pointing at that interface. Nullable,ImplicitUsingsandLangVersion latestare enabled.- New
usingdirectives go into theGlobalUsings.csof the respective project, inside the existing#pragma warning disable IDE0065block, never at the top of a file. The editorconfig requires usings inside the namespace (csharp_using_directive_placement=inside_namespace:warning), which global usings cannot satisfy, that is what the pragma is for. Do not add other pragmas except theSerilog004ones the tests already use. The comment text in that block is German because Visual Studio generated it, leave it alone. - Fields, properties, methods and events are always accessed with
this.qualification (dotnet_style_qualification_for_*at severitywarning). src/.editorconfigalso enforces braces everywhere, no multiple blank lines, four spaces, CRLF, UTF-8, file scoped namespaces,Systemusings sorted first andIDE0005as warning. Analyzer warnings are fixed, not silenced.- The
.csprojfiles are indented with four spaces, unlike the Visual Studio default of two.
Do not silently "clean up" these, they are existing behaviour:
- The webhook environment variables hijack the tests.
TestHelperreadsMicrosoftTeamsWebhookUrlandMicrosoftTeamsWebhookUrlPowerAutomateand only falls back tohttp://localhost:63210when they are empty. If either variable is set on the machine, the test run posts real messages into a real Teams channel and every assertion against the WireMock server fails, because nothing ever reaches it. Check the variables before debugging a red test run. - The mock server port is fixed.
TestHelper.MockServerPortis63210and every test starts its ownWireMockServeron it. Two test runs at the same time, or any other process holding that port, break the run. This is also why the tests must not be parallelized. RootNamespaceisSerilog, not the assembly name. The extension class therefore lives in namespaceSerilogat the project root, so thatWriteTo.MicrosoftTeams(...)is available without an extra using. Everything else sits in the folder chainSinks/MicrosoftTeams/Alternative/..., which mirrors the namespaceSerilog.Sinks.MicrosoftTeams.Alternative.- Two payload formats in one sink.
UsePowerAutomateWorkflowsdecides betweenCreateMessageCard(the legacyMessageCardJSON for O365 incoming webhooks) andCreateMessage(anAdaptiveCards.AdaptiveCardinside an attachment, for Power Automate workflow URLs). Both paths build title, text, facts and buttons separately, a change to one of them usually has to be made in the other as well. - AdaptiveCards types are always written with their namespace prefix
(
AdaptiveCards.AdaptiveTextBlock), there is deliberately no global using for that namespace, so that the adaptive card code is recognizable at a glance. - Events are grouped by exception message only.
GetMessagesToSendmerges a log event into an earlier one of the same batch when both exception messages are equal, no matter how different the rendered messages are. The merged event keeps the first one's text and only widens the first and last occurrence. With the defaultBatchSizeLimitof 1 this never happens. System.Net.Httpis deliberately not referenced any more. The package was added in version 1.4.1.0/1.4.2.0 against theSystem.Runtimeloading issue #30. On net8.0 and later the framework provides those assemblies, the reference did nothing, and the Net 10 SDK rejects it withNU1510. Do not add it back, add aNoWarnfor it even less.- The tests close their logger, they do not sleep. Every test ends with
CloseLogger, which disposes the logger and thereby flushes the batching sink.Thread.SleepplusLog.CloseAndFlush()used to stand there, which was wrong twice over:Log.Loggeris never assigned by these tests, and one second is not enough to drain a queue that the sink empties one event per second. Leftover events then arrived at the mock server of the following test and made its count assertion fail. - The
Serilog004pragmas in the tests are required. Several tests build their message template from aGuidat runtime, which the Serilog analyzer rejects. The pragma pairs sit around exactly those calls, do not widen them to whole files. failureCallbackis gone but still documented. The option was removed in commit2f3e3c0, the row inHowToUse.mdremains as struck through text so that readers of older versions find the replacement (fallback logging, see https://nblumhardt.com/2024/10/fallback-logging/).Updating.md,HowToUse.mdandREADME.mdeach own a part of the documentation. The option table lives inHowToUse.mdonly, the README links to it. A new sink option has to be added to the option table, bothMicrosoftTeams(...)overloads andMicrosoftTeamsSinkOptions.- The contributor table is generated. The block between the
ALL-CONTRIBUTORS-LISTmarkers inREADME.mdand the file.all-contributorsrcbelong to the all-contributors bot. Do not edit them by hand. - The repository was renamed and transferred. It started as
SeppPenner/Serilog.Sinks.MicrosoftTeamsand lives atserilog-contrib/Serilog.Sinks.MicrosoftTeams.Alternativesince version 1.2.7.0. GitHub answers the old path with a 301 and git follows it, so an old clone keeps working and only prints "This repository moved" on every push. If a clone still has the old URL, fix the clone (git remote set-url), never the URLs in the files. BuildAndPushPackage.batmust not echo the API key. The script runs with@ECHO on, so every line without a leading@ends up in the console output, including the expanded%NUGET_API_KEY%. The twodotnet nuget pushlines therefore carry an@. Keep it that way, and do not paste console output of that script anywhere.- The two batch files must run from the repository root. Both start with
cd .\src, that path is relative to the working directory, not to the script. - The changelog version has four parts, the tag has three.
1.5.0.0inChangelog.mdand inPackageReleaseNotes,1.5.0as the tag.
- Make the change.
- Add an entry at the top of
Changelog.mdin the existing format:* **Version 1.6.0.0 (2026-08-17)**: Short description. - Put the same text into
PackageReleaseNotesinsrc/Serilog.Sinks.MicrosoftTeams.Alternative/Serilog.Sinks.MicrosoftTeams.Alternative.csproj, in the formatVersion 1.6.0.0 (2026-08-17): Short description.. - Commit that.
- Tag the commit with the plain version number, no
vprefix (1.5.0,1.4.9, ...). The existing tags are lightweight tags, create new ones the same way. - Push the commits and the tag.
- Only now run
BuildAndPushPackage.batfrom the repository root. It deletes everybinandobjbelowsrc, builds in Release and pushes*.nupkgand*.snupkgto nuget.org with%NUGET_API_KEY%.
The tag has to exist before the package build, otherwise GitVersion burns a prerelease version
such as 1.6.0-1+Branch.master.Sha... into the published package. Updating.md describes the same
order in shorter words.
- Never amend a commit. No
git commit --amend, not for a typo in the message, not to add a forgotten file, not even when the commit is still local. Write a follow-up commit instead. The release versions come from tags on exact commits, an amended commit leaves its tag pointing at a commit that no longer exists in the branch.
- Commit messages are written in English only: short, precise subject line, explanatory body when needed.
- Code comments and comments in project files such as
.csprojare always English, regardless of the language used in the conversation. - No em dashes or en dashes (
—,–), neither in prose, commit messages, code comments nor documentation. Use a regular hyphen, comma, colon, parentheses or a separate sentence. - German texts (documentation, chat replies) always use real umlauts and ß, never ASCII
transliterations such as
ae,oe,ueorss. Identifiers, file names and configuration keys stay unchanged where umlauts are technically undesirable.