SFMCSDK.Net 4.0.1.2

dotnet add package SFMCSDK.Net --version 4.0.1.2
                    
NuGet\Install-Package SFMCSDK.Net -Version 4.0.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SFMCSDK.Net" Version="4.0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SFMCSDK.Net" Version="4.0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="SFMCSDK.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SFMCSDK.Net --version 4.0.1.2
                    
#r "nuget: SFMCSDK.Net, 4.0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SFMCSDK.Net@4.0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SFMCSDK.Net&version=4.0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=SFMCSDK.Net&version=4.0.1.2
                    
Install as a Cake Tool

SFMCSDK.Net

NuGet release Targets: net8.0 | net9.0 | net10.0 SFMCSDK 4.0.1 sfmcsdk 3.1.1 Licence: MIT

One Salesforce Marketing Cloud SFMC SDK API for .NET. Awaitable initialization, identity (contact key and attributes) and custom event tracking on Android and iOS, written once in shared code — over the SFMCSDK.Net.Android and SFMCSDK.Net.iOS bindings.

dotnet add package SFMCSDK.Net
using SFMCSDK.Net;

var sdk = new SfmcSdkClient();          // one per process - register it as a DI singleton

await sdk.InitializeAsync(new SfmcSdkOptions { LogLevel = SfmcLogLevel.Debug });

sdk.Identity.SetProfileId("contact-key");
sdk.Identity.SetAttribute("plan", "pro");

sdk.TrackCustomEvent("checkout_started", new Dictionary<string, string>
{
    ["cart_size"] = "3",
});

That is the whole shared-code surface, and it is the same call sites on both platforms — no #if, no per-platform adapter.


Contents


Why there is a cross-platform layer

The two platform bindings are faithful projections of what Salesforce ships, and consequently share almost no shape. The same three operations, raw:

Operation Android (Com.Salesforce.Marketingcloud.Sfmcsdk) iOS (SFMCSDK)
Initialize SFMCSdk.Configure(context, config, status => …) — needs a Context, reports one terminal status through a Kotlin Function1 SFMCSdk.InitializeSdk(config, statuses => …) — all static, reports per-module statuses through a block
Identity sdk.Identity = sdk.Identity.NewBuilder().SetProfileId(x).Build() — an immutable record on an instance that RequestSdk hands out asynchronously SFMCSdk.Identity.Edit(m => { m.ProfileId = x; return m; }) — a static object edited in place through a modifier closure
Track SFMCSdk.Track(EventManager.CustomEvent(name, attrs)) — attributes are IDictionary<string, Java.Lang.Object> SFMCSdk.Track(new SFMCSdkCustomEvent(name, attrs)) — attributes are NSDictionary<NSString, NSObject>

Writing that twice per app is the tax this package removes. It also files off the sharp edges the raw surfaces leave: a custom event whose name the native SDK rejects throws here (Android's factory returns null and the event silently vanishes; iOS's initializer returns nil), and an initialization callback that never arrives times out with an exception naming the operation instead of hanging the app's startup await forever.

And one edge is upstream behaviour this repository had to measure to hide: with zero modules configured, iOS's initializeSdk completion never fires — it reports per-module statuses, and there are none. InitializeAsync knows that (see Platforms/Apple), so the same await completes on both platforms.

What the façade does and does not hide

The façade carries what an app does from shared code — initialize once, identify the user, track events, read a diagnostic line — and deliberately nothing else. It adds no abstraction over things only one platform has, and it does not wrap surfaces an app touches from platform code anyway.

Everything else stays reachable, because the packages underneath arrive with this one:

  • Android: namespace Com.Salesforce.Marketingcloud.Sfmcsdk (package SFMCSDK.Net.Android) — module configs, the event bus, behaviors, encrypted storage, the in-app messaging models, RequestSdk itself.
  • iOS: namespace SFMCSDK (package SFMCSDK.Net.iOS) — the config builder's per-module setters, consent/CDP, keychain helpers, the full event model.

Mixing is fine and expected: initialize and track through the façade, and configure a module or subscribe to the event bus through the raw namespace in the same app. The façade builds an empty module config on both platforms — module packages (MobilePush and friends) are configured through the raw builders, which is where their options live.

On the plain net8.0/net9.0/net10.0 target frameworks the package restores and compiles — that is what lets a shared class library, a unit test, or a MAUI app's Windows head reference it unconditionally — and every member that would reach the native SDK throws PlatformNotSupportedException naming where the real implementation lives. Throwing, not no-oping: an identity edit that silently vanished on a Windows head would read as data loss in Marketing Cloud.

Two members are exempt and never throw, on any target framework, because they are what shared code branches on: IsSupported (is there a native SDK under this build?) and IsInitialized (did an InitializeAsync on this client succeed?). For a head that has no SDK and wants the calls to be harmless rather than fatal, the package also ships NullSfmcSdkClient — a no-op implementation that still validates its arguments, so it hides platforms without hiding your bugs:

builder.Services.AddSingleton<ISfmcSdkClient>(
    new SfmcSdkClient() is { IsSupported: true } client ? client : new NullSfmcSdkClient());

Using MobilePush too? Then you do not need this client at all: MarketingCloudSDK.Net composes one internally and re-exports Identity and TrackCustomEvent, and its initialization brings this core up underneath with the MobilePush module attached. Never call this package's InitializeAsync in an app that initializes MobilePush — that configures the core a second time with an empty module set, which upstream forbids.

Packages and versions

One package. The version is <SFMCSDK iOS version>.<binding revision>4.0.1.2 is SFMCSDK 4.0.1, revision 2, and the Android side of the same release is sfmcsdk 3.1.1.

Why one version names one SDK. Salesforce releases the iOS and Android SDKs on separate cadences and their version numbers have never matched. A façade over both has to pick one line to name itself after or invent a third numbering that maps to nothing. It picks iOS — the same convention the sibling DatadogNet façade uses — and states the Android version everywhere it states its own.

SFMCSDK.Net SFMCSDK (iOS, native) sfmcsdk (Android, native) SFMCSDK.Net.iOS SFMCSDK.Net.Android
4.0.1.2 4.0.1 3.1.1 4.0.1.2 3.1.1.1

The platform packages are pinned exactly ([4.0.1.2] / [3.1.1.1]), not floored: the façade calls each binding's hand-written convenience layer — the Action overloads of Configure/RequestSdk on Android, the Func-typed Identity.Edit trampoline on iOS — and those carry no compatibility promise across binding revisions. A newer binding is consumed by this repository re-pinning and releasing, with the device suites in between, not by NuGet floating a consumer onto it.

Installing

<PackageReference Include="SFMCSDK.Net" Version="4.0.1.2" />

Nine target frameworks: net8.0, net9.0, net10.0, each with its -android and -ios head — net8.0-android34.0, net8.0-ios18.0, net9.0-android35.0, net9.0-ios18.0, net10.0-android36.0, net10.0-ios26.0. Floors: iOS 12.2 (the SFMC framework is Swift and relies on the OS Swift runtime, ABI-stable from 12.2), Android API 26 (the sfmcsdk .aar manifest's own floor).

The platform heads pull SFMCSDK.Net.Android 3.1.1.1 / SFMCSDK.Net.iOS 4.0.1.2 transitively; apps reference only this package unless they want the raw namespaces pinned explicitly (they may — the same versions arrive either way).

For a MAUI app with an Android head, target net10: MAUI 8 and 9 cannot build against the AndroidX generation the SFMC Android binding's dependencies resolve to (a Java-callable-wrapper defect the binding repository's README records), while MAUI 10 handles it. An iOS-only MAUI app is fine on net8 or net9. The net8 and net9 assets are otherwise for plain .NET Android / .NET iOS apps, which is also what the binding packages' own net8 support is for.

Usage notes

  • One client per process. The native SDK on both platforms is a process-wide singleton behind static entry points, so SfmcSdkClient is meant to live as a DI singleton. The initialization guard is per-instance — the instance is what promises one-shot semantics.
  • InitializeAsync is one-shot, even on failure. A second call throws InvalidOperationException. The guard deliberately does not reset on failure: the native configure is itself not retryable, and after a timeout the first attempt may still be running — a retry would race it. A process that needs a fresh attempt restarts.
  • Identity is fire-and-forget, before or after initialization — both SDKs queue early identity work. Nothing waits for a server acknowledgement; changes batch on the SDK's own schedule.
  • DiagnosticState is for logs, never for parsing. iOS returns the SDK's state JSON; Android's equivalent detail lives on the instance requestSdk delivers asynchronously, so the synchronous property reports the static initialization state (NONE/INITIALIZING/READY/ERROR) instead.
  • Push is not here. MobilePush lives in the MarketingCloudSDK binding repositories, which depend on these same core bindings. For a cross-platform MobilePush surface use MarketingCloudSDK.Net — it composes this façade rather than sitting beside it; for module-level control, the raw config builders.
  • IsSupported and IsInitialized never throw. They are the two members shared code branches on, so they answer on every target framework — including the neutral ones, where everything else throws. IsInitialized is a status signal, not a precondition: identity work is legal before initialization because the SDK queues it. It is also not the one-shot guard — after a failed initialization it stays false while the guard stays claimed.

How this repository works

Nothing is bound here and nothing native is committed. The repository compiles one multi-targeted assembly against the two pinned binding packages: shared sources declare the API and private partial …Core seams, and exactly one of Platforms/Android, Platforms/Apple or Platforms/Neutral supplies the bodies per target framework (see src/Sfmc.Facade.props).

Each .NET SDK's android/ios workloads ship reference packs for only two target frameworks — the .NET 9 band covers net8/net9, the .NET 10 band covers net9/net10 — so build/BuildNugets.sh packs twice and build/merge-packages.py grafts the net10 assets and their dependency groups into one package. The groups matter as much as the assemblies: an empty net10 group would tell NuGet a net10 consumer needs no binding underneath, and the app would fail with the native SDK missing.

The pins live in Directory.Build.props as literal properties (SfmcAndroidPackageVersion, SfmcIosPackageVersion) that the scripts and workflows sed-parse. NuGet.config adds ./artifacts as a package source, so locally packed platform bindings (from the sibling repositories) and the locally packed façade both resolve without publishing anything.

Layout

Path What
src/Sfmc.Facade.props TFM bands, per-platform source selection, compiler settings, pack assets
src/SFMCSDK.Net/ The façade: shared half + Platforms/{Android,Apple,Neutral}
build/ Pack, merge, README-check and upstream-check scripts; packages.tsv is the roster
tests/ UnitTests (neutral leg, no workloads), PackageTests (nupkg shape), DeviceTests (one project, two heads, driving the façade over the packed package)
samples/ A MAUI app driving initialization, identity and events through the façade, zero #if
.github/workflows/ pr, build, release, auto-release

Building locally

cp ../SFMCSDK.Net.Android/artifacts/*.nupkg ../SFMCSDK.Net.iOS/artifacts/*.nupkg artifacts/  # or let nuget.org serve them
./build/BuildNugets.sh                # packs 4.0.1.2 into ./artifacts
dotnet test tests/SFMCSDK.Net.UnitTests -p:SfmcNeutralOnly=true
dotnet test tests/SFMCSDK.Net.PackageTests

Tests

Three suites, cheapest first — each catches what the previous one cannot see:

dotnet test tests/SFMCSDK.Net.UnitTests -p:SfmcNeutralOnly=true   # validation, guard, neutral contract
dotnet test tests/SFMCSDK.Net.PackageTests                        # nine TFMs, exact pins, licence, symbols
./.github/scripts/run-simulator-tests.sh 4.0.1.2 net9.0-ios18.0   # the façade over the real SDK
./.github/scripts/run-emulator-tests.sh 4.0.1.2 net9.0-android35.0

-p:SfmcNeutralOnly=true collapses the referenced façade to its neutral target frameworks, so the unit tests restore and run on any machine with no mobile workloads — it is the pipeline's fastest signal, and must never be set on a pack (it would produce a package with no platform legs).

The device checks run without credentials on purpose: the empty module config points at no tenant, so server calls fail by design. What they prove is the packaging promise — the packed package's dependency groups pull the right binding in on each platform, and the façade drives it: InitializeAsync completes, identity and tracking cross into native code, the one-shot guard holds, and DiagnosticState answers.

Upgrading

Re-pin, verify, release — in that order:

  1. Bump SfmcAndroidPackageVersion / SfmcIosPackageVersion (and, when the native line moved, SfmcNativeVersion + SfmcAndroidNativeVersion) in Directory.Build.props.
  2. Update the version map above — ./build/CheckReadmeVersions.sh fails the build until the README agrees with the props, which is the point.
  3. Reset SfmcBindingRevision to 1 on a native bump, or increment it for a façade-only change.
  4. Open a PR: the pipeline packs, validates the package shape and runs both device suites against the new pins before anything ships.

./build/check-upstream.sh (shared verbatim with the binding repositories, driven by build/upstream.tsv) reports when Salesforce publishes a native version newer than the pinned lines — the early signal that the binding repositories will move and this façade will need re-pinning behind them.

Releasing

Merging a file named docs/release-notes/<four-part-version>.md to main is the release: auto-release tags it, release verifies the tag is on the default branch (the guard), packs with verification off — the tagged commit was already verified on its pull request — and publishes to nuget.org via trusted publishing (OIDC, no stored API key). Every pull request publishes a -beta.<pr>.<run> prerelease.

Troubleshooting

PlatformNotSupportedException mentioning the neutral build. The code ran on a plain target framework (a unit test, a Windows head). That is the documented contract — construct freely, guard the calls, or inject a fake of ISfmcSdkClient; the real implementation runs in net*-android / net*-ios heads.

InvalidOperationException from a second InitializeAsync. Initialization is one-shot per client and per process — see Usage notes. Await the first call; do not retry.

Restore fails with NU1301 naming artifacts. The local package source must exist: mkdir -p artifacts (a fresh clone has it via the committed .gitkeep).

A stale package in artifacts/ shadows nuget.org, and NU1107 blames a version you never pinned. The local feed is searched alongside nuget.org, so a nupkg packed here before a dependency was re-pinned keeps being resolved under the same version number — and once restored it is cached in the NuGet global-packages folder, where it poisons every other repository too. Clearing both is the fix:

rm -f artifacts/*.nupkg artifacts/*.snupkg && rm -rf ~/.nuget/packages/sfmcsdk.net/<version> && dotnet restore --force

The tell is a NU1107 naming two versions of the same binding, one of which matches no Directory.Build.props pin in any repository.

NU1608 warnings about AndroidX Lifecycle versions. A property of the AndroidX graph the SFMC Android binding pulls in — two of its packages exact-range a sibling that a third floats past. NuGet resolves the higher version and the result is what the binding's emulator tests pass against. This repository suppresses the warning rather than pinning AndroidX for every consumer; pin in your app if you want it gone.

A surface you need is missing from the façade. Deliberate — see What the façade does and does not hide. Use the platform namespaces; they ship in the same restore.

Licence

The code in this repository is MIT, and the package declares plain MIT — it ships no native binaries, so Salesforce's licence is not its to declare. The binding packages underneath ship the native SFMC SDK artifacts (© Salesforce, BSD-3-Clause) and each declares MIT AND BSD-3-Clause with both texts packed; the BSD text is mirrored at licenses/BSD-3-Clause-Salesforce.txt for reference.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-android34.0 is compatible.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-ios18.0 is compatible.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-android35.0 is compatible.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-ios18.0 is compatible.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-android36.0 is compatible.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-ios26.0 is compatible.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on SFMCSDK.Net:

Package Downloads
MarketingCloudSDK.Net

One cross-platform API for Salesforce Marketing Cloud MobilePush: awaitable initialization with real credentials, identity (contact key and attributes) via the SFMC SDK core, registration reads and tag/attribute edits on Android and iOS from shared code, over the MarketingCloudSDK.Net.Android and MarketingCloudSDK.Net.iOS bindings. Restores on plain target frameworks too, so shared class libraries and unit tests need no platform conditionals.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.1.2 28 7/30/2026
4.0.1.2-beta.4.12 29 7/30/2026
4.0.1.1 118 7/30/2026
4.0.1.1-beta.3.10 28 7/30/2026
4.0.1.1-beta.2.7 29 7/30/2026
4.0.1.1-beta.2.5 35 7/30/2026
1.1.3.8 271 10/30/2024
1.1.3.6 203 10/7/2024
1.1.2.10 202 7/12/2024
1.1.2.4 198 7/12/2024
1.1.2.3 183 5/20/2024
1.1.2.2 191 5/17/2024
1.1.1.80 215 2/28/2024
1.1.1.79 237 2/22/2024
1.1.1.1 222 2/12/2024
1.0.3.73 253 2/7/2024

## What's changed

A façade-only revision on the same native line: SFMCSDK stays **4.0.1** on iOS and sfmcsdk **3.1.1**
on Android, and the pinned bindings are unchanged (`SFMCSDK.Net.iOS 4.0.1.2`,
`SFMCSDK.Net.Android 3.1.1.1`).

### Added

- **`IsSupported` and `IsInitialized`** on `ISfmcSdkClient` — the two members that answer on every
 target framework instead of throwing, so shared code has something to branch on. Previously the
 only readback was `DiagnosticState`, which the docs forbid parsing and which throws on the neutral
 heads, so "is it up yet?" could not be answered from shared code at all. Neither is the one-shot
 guard: after a failed initialization `IsInitialized` stays false while the guard stays claimed.
- **`NullSfmcSdkClient`** — the no-op implementation the README used to tell consumers to write
 themselves, for MAUI heads with no SDK and for tests. Its argument validation mirrors the real
 façade member for member, so it hides platforms without hiding caller bugs.
- **`build/RepinPackages.sh`** — the re-pin helper the upstream-drift issue template has been
 pointing at under a name that did not exist (`BumpNativeVersion.sh`). It rewrites the props and
 reports what the README still owes, leaving `CheckReadmeVersions.sh` as the gate.

### Documentation

- The MAUI target-framework guidance said "target net9 or net10" and then explained that MAUI 9
 cannot work — it is net10 for an Android head, net8/net9 fine for an iOS-only app.
- Stated the relationship with `MarketingCloudSDK.Net` in both directions: an app using MobilePush
 needs only that package, and must never call **this** package's `InitializeAsync`, because the
 MobilePush façade already configures the core with its module attached.
- Added the stale-`artifacts/` troubleshooting entry: a locally packed nupkg keeps being resolved
 under a version that also exists on nuget.org, and the NuGet global-packages cache spreads it to
 every sibling repository.

### Upgrading

Source-compatible for consumers. Implementers of `ISfmcSdkClient` — a hand-written test fake — must
add `IsSupported` and `IsInitialized`, or switch to `NullSfmcSdkClient`.