|
| 1 | +--- |
| 2 | +title: "What are your ASP.NET Core users up to? Find out with PostHog." |
| 3 | +description: "PostHog is a product analytics platform that helps you build better products faster. It contains a set of tools to help you capture analytics and leverage feature flags." |
| 4 | +tags: [career, work] |
| 5 | +excerpt_image: https://github.com/user-attachments/assets/b7c64431-0232-4707-b016-c0161ea6b0eb |
| 6 | +--- |
| 7 | + |
| 8 | +PostHog helps you build better products. It tracks what users do. It controls features in production. And now it works with .NET! |
| 9 | + |
| 10 | +[I joined PostHog at the beginning of the year](https://haacked.com/archive/2025/01/07/new-year-new-job/) as a Product Engineer on [the Feature Flags team](https://posthog.com/teams/feature-flags). Feature flags are just one of the many tools PostHog offers to help product engineers build better products. |
| 11 | + |
| 12 | +Much of my job will consist of writing Python and React with TypeScript. But when I started, I noticed they didn't have a .NET SDK. It turns out, I know a thing or two about .NET! |
| 13 | + |
| 14 | + |
| 15 | + |
| 16 | +So if you've been wanting to use PostHog in your ASP.NET Core applications, yesterday is your lucky day! The 1.0 version of the PostHog .NET SDK for ASP.NET Core is [available on NuGet](https://www.nuget.org/packages/PostHog.AspNetCore). |
| 17 | + |
| 18 | +```bash |
| 19 | +dotnet add package PostHog.AspNetCore |
| 20 | +``` |
| 21 | + |
| 22 | +You can find documentation for the library on [the PostHog docs site](https://posthog.com/docs/libraries/dotnet), but I'll cover some of the basics here. I'll also cover non-ASP.NET Core usage later in this post. |
| 23 | + |
| 24 | +## Configuration |
| 25 | + |
| 26 | +To configure the client SDK, you'll need: |
| 27 | + |
| 28 | +1. Project API Key - _from the [PostHog dashboard](https://us.posthog.com/settings/project)_ |
| 29 | +2. Personal API Key - _for local evaluation (Optional, but recommended)_ |
| 30 | + |
| 31 | +> [!NOTE] |
| 32 | +> For better performance, enable local feature flag evaluation by adding a personal API key (found in Settings). This avoids making API calls for each flag check. |
| 33 | +
|
| 34 | +By default, the PostHog client looks for settings in the `PostHog` section of the configuration system such as in the `appSettings.json` file: |
| 35 | + |
| 36 | +```json |
| 37 | +{ |
| 38 | + "PostHog": { |
| 39 | + "ProjectApiKey": "phc_..." |
| 40 | + } |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +Treat your personal API key as a secret by using a secrets manager to store it. For example, for local development, use the `dotnet user-secrets` command to store your personal API key: |
| 45 | + |
| 46 | +```bash |
| 47 | +dotnet user-secrets init |
| 48 | +dotnet user-secrets set "PostHog:PersonalApiKey" "phx_..." |
| 49 | +``` |
| 50 | + |
| 51 | +In production, you might use Azure Key Vault or a similar service to provide the personal API key. |
| 52 | + |
| 53 | +## Register the client |
| 54 | + |
| 55 | +Once you set up configuration, register the client with the dependency injection container. |
| 56 | + |
| 57 | +In your `Program.cs` file, call the `AddPostHog` extension method on the `WebApplicationBuilder` instance. It'll look something like this: |
| 58 | + |
| 59 | +```csharp |
| 60 | +using PostHog; |
| 61 | + |
| 62 | +var builder = WebApplication.CreateBuilder(args); |
| 63 | + |
| 64 | +builder.AddPostHog(); |
| 65 | +``` |
| 66 | + |
| 67 | +Calling `builder.AddPostHog()` adds a singleton implementation of `IPostHogClient` to the dependency injection container. Inject it into your controllers or pages like so: |
| 68 | + |
| 69 | +```csharp |
| 70 | +public class MyController(IPostHogClient posthog) : Controller |
| 71 | +{ |
| 72 | +} |
| 73 | + |
| 74 | +public class MyPage(IPostHogClient posthog) : PageModel |
| 75 | +{ |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +## Usage |
| 80 | + |
| 81 | +Use the `IPostHogClient` service to identify users, capture analytics, and evaluate feature flags. |
| 82 | + |
| 83 | +Use the `IdentifyAsync` method to identify users: |
| 84 | + |
| 85 | +```csharp |
| 86 | +// This stores information about the user in PostHog. |
| 87 | +await posthog.IdentifyAsync( |
| 88 | + distinctId, |
| 89 | + user.Email, |
| 90 | + user.UserName, |
| 91 | + // Properties to set on the person. If they're already |
| 92 | + // set, they will be overwritten. |
| 93 | + personPropertiesToSet: new() |
| 94 | + { |
| 95 | + ["phone"] = user.PhoneNumber ?? "unknown", |
| 96 | + ["email_confirmed"] = user.EmailConfirmed, |
| 97 | + }, |
| 98 | + // Properties to set once. If they're already set |
| 99 | + // on the person, they won't be overwritten. |
| 100 | + personPropertiesToSetOnce: new() |
| 101 | + { |
| 102 | + ["joined"] = DateTime.UtcNow |
| 103 | + }); |
| 104 | +``` |
| 105 | +Some things to note about the `IdentifyAsync` method: |
| 106 | + |
| 107 | +- The `distinctId` is the identifier for the user. This could be an email, a username, or some other identifier such as the database Id. The important thing is that it's a consistent and unique identifier for the user. If you use PostHog on the client, use the same `distinctId` here as you do on the client. |
| 108 | +- The `personPropertiesToSet` and `personPropertiesToSetOnce` are optional. You can use them to set properties about the user. |
| 109 | +- If you choose a `distinctId` that can change (such as username or email), you can use the `AliasAsync` method to alias the old `distinctId` with the new one so that the user can be tracked across different `distinctIds`. |
| 110 | + |
| 111 | +To capture an event, call the `Capture` method: |
| 112 | + |
| 113 | +```csharp |
| 114 | +posthog.Capture("some-distinct-id", "my-event"); |
| 115 | +``` |
| 116 | + |
| 117 | +This will capture an event with the distinct id, the event name, and the current timestamp. You can also include properties: |
| 118 | + |
| 119 | +```csharp |
| 120 | +posthog.Capture( |
| 121 | + "some-distinct-id", |
| 122 | + "user signed up", |
| 123 | + new() { ["plan"] = "pro" }); |
| 124 | +``` |
| 125 | + |
| 126 | +The `Capture` method is synchronous and returns immediately. The actual batching and sending of events is done in the background. |
| 127 | + |
| 128 | +## Feature flags |
| 129 | + |
| 130 | +To evaluate a feature flag, call the `IsFeatureEnabledAsync` method: |
| 131 | + |
| 132 | +```csharp |
| 133 | +if (await posthog.IsFeatureEnabledAsync( |
| 134 | + "new_user_feature", |
| 135 | + "some-distinct-id")) { |
| 136 | + // The feature flag is enabled. |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +This will evaluate the feature flag and return `true` if the feature flag is enabled. If the feature flag is not enabled or not found, it will return `false`. |
| 141 | + |
| 142 | +Feature Flags can contain filter conditions that might depend on properties of the user. For example, you might have a feature flag that is enabled for users on the pro plan. |
| 143 | + |
| 144 | +If you've previously identified the user and are NOT using local evaluation, the feature flag is evaluated on the server against the user properties set on the person via the `IdentifyAsync` method. |
| 145 | + |
| 146 | +But if you're using local evaluation, the feature flag is evaluated on the client, so you have to pass in the properties of the user: |
| 147 | + |
| 148 | +```csharp |
| 149 | +await posthog.IsFeatureEnabledAsync( |
| 150 | + featureKey: "person-flag", |
| 151 | + distinctId: "some-distinct-id", |
| 152 | + personProperties: new() { ["plan"] = "pro" }); |
| 153 | +``` |
| 154 | + |
| 155 | +This will evaluate the feature flag and return `true` if the feature flag is enabled and the user's plan is "pro". |
| 156 | + |
| 157 | +## .NET Feature Management |
| 158 | + |
| 159 | +[.NET Feature Management](https://learn.microsoft.com/en-us/azure/azure-app-configuration/feature-management-dotnet-reference) is an abstraction over feature flags that is supported by ASP.NET Core. With it enabled, you can use the `<feature />` tag helper to conditionally render UI based on the state of a feature flag. |
| 160 | + |
| 161 | +```csharp |
| 162 | +<feature name="my-feature"> |
| 163 | + <p>This is a feature flag.</p> |
| 164 | +</feature> |
| 165 | +``` |
| 166 | + |
| 167 | +You can also use the `FeatureGateAttribute` in your controllers and pages to conditionally execute code based on the state of a feature flag. |
| 168 | + |
| 169 | +```csharp |
| 170 | +[FeatureGate("my-feature")] |
| 171 | +public class MyController : Controller |
| 172 | +{ |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +If your app already uses .NET Feature Management, you can switch to using PostHog with very little effort. |
| 177 | + |
| 178 | +To use PostHog feature flags with the .NET Feature Management library, implement the `IPostHogFeatureFlagContextProvider` interface. The simplest way to do that is to inherit from the `PostHogFeatureFlagContextProvider` class and override the `GetDistinctId` and `GetFeatureFlagOptionsAsync` methods. This is required so that .NET Feature Management can evaluate feature flags locally with the correct `distinctId` and `personProperties`. |
| 179 | + |
| 180 | +```csharp |
| 181 | +public class MyFeatureFlagContextProvider( |
| 182 | + IHttpContextAccessor httpContextAccessor) |
| 183 | + : PostHogFeatureFlagContextProvider |
| 184 | +{ |
| 185 | + protected override string? GetDistinctId() => |
| 186 | + httpContextAccessor.HttpContext?.User.Identity?.Name; |
| 187 | + |
| 188 | + protected override ValueTask<FeatureFlagOptions> GetFeatureFlagOptionsAsync() |
| 189 | + { |
| 190 | + // In a real app, you might get this information from a database or other source for the current user. |
| 191 | + return ValueTask.FromResult( |
| 192 | + new FeatureFlagOptions |
| 193 | + { |
| 194 | + PersonProperties = new Dictionary<string, object?> |
| 195 | + { |
| 196 | + |
| 197 | + ["plan"] = "pro" |
| 198 | + }, |
| 199 | + OnlyEvaluateLocally = true |
| 200 | + }); |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +Then, register your implementation in `Program.cs` (or `Startup.cs`): |
| 206 | + |
| 207 | +```csharp |
| 208 | +using PostHog; |
| 209 | + |
| 210 | +var builder = WebApplication.CreateBuilder(args); |
| 211 | + |
| 212 | +builder.AddPostHog(options => { |
| 213 | + options.UseFeatureManagement<MyFeatureFlagContextProvider>(); |
| 214 | +}); |
| 215 | +``` |
| 216 | + |
| 217 | +This registers a feature flag provider that uses your implementation of `IPostHogFeatureFlagContextProvider` to evaluate feature flags against PostHog. |
| 218 | + |
| 219 | +## Non-ASP.NET Core usage |
| 220 | + |
| 221 | +The `PostHog.AspNetCore` package adds ASP.NET Core specific functionality on top of the core `PostHog` package. But if you're not using ASP.NET Core, you can use the core `PostHog` package directly: |
| 222 | + |
| 223 | +```bash |
| 224 | +dotnet add package PostHog.AspNetCore |
| 225 | +``` |
| 226 | + |
| 227 | +And then register it with your dependency injection container: |
| 228 | + |
| 229 | +```csharp |
| 230 | +builder.Services.AddPostHog(); |
| 231 | +``` |
| 232 | + |
| 233 | +If you're not using dependency injection, you can still use the registration method: |
| 234 | + |
| 235 | +```csharp |
| 236 | +using PostHog; |
| 237 | +var services = new ServiceCollection(); |
| 238 | +services.AddPostHog(); |
| 239 | +var serviceProvider = services.BuildServiceProvider(); |
| 240 | +var posthog = serviceProvider.GetRequiredService<IPostHogClient>(); |
| 241 | +``` |
| 242 | + |
| 243 | +For a console app (or apps not using dependency injection), you can also use the `PostHogClient` directly, just make sure it's a singleton: |
| 244 | + |
| 245 | +```csharp |
| 246 | +using System; |
| 247 | +using PostHog; |
| 248 | + |
| 249 | +var posthog = new PostHogClient( |
| 250 | + Environment.GetEnvironmentVariable("PostHog__PersonalApiKey")); |
| 251 | +``` |
| 252 | + |
| 253 | +## Examples |
| 254 | + |
| 255 | +To see all this in action, the [`posthog-dotnet` GitHub repository](https://github.com/posthog/posthog-dotnet) has a [samples directory](https://github.com/PostHog/posthog-dotnet/tree/main/samples) with a growing number of example projects. For example, the [HogTied.Web](https://github.com/PostHog/posthog-dotnet/tree/main/samples/HogTied.Web) project is an ASP.NET Core web app that uses PostHog for analytics and feature flags and shows some advanced configuration. |
| 256 | + |
| 257 | +## What's next? |
| 258 | + |
| 259 | +With this release done, I'll be focusing my attention on the Feature Flags product. Even so, I'll continue to maintain the SDK and fix any reported bugs. |
| 260 | + |
| 261 | +If anyone reports bugs, I'll be sure to fix them. But I won't be adding any new features for the moment. |
| 262 | + |
| 263 | +Down the road, I'm hoping to add a `PostHog.Unity` package. I just don't have a lot of experience with Unity yet. My game development experience mostly consists of getting shot in the face by squaky voiced kids playing Fortnite. I'm hoping someone will contqribute a Unity sample project to the repo which I can use as a starting point. |
| 264 | + |
| 265 | +If you have any feedback, questions, or issues with the PostHog .NET SDK, please reach file an issue at https://github.com/PostHog/posthog-dotnet. |
0 commit comments