How to upgrade the .NET SDK from v1 to v2

This guide will help you navigate through changes included in version 2 of the Account Protect .NET SDK for a successful upgrade.

Version 2 is generated from the Account Protect OpenAPI specification, which changes the client, event, and response types throughout the SDK.

📘

Documentation for version 2.x.x

Refer to the new documentation for detailed instructions on how to use version 2 of the SDK.

Overview of changes

  • Update client setup to a builder pattern with Client.Builder(...), replacing AddDataDome dependency injection registration and DataDomeOptions.
  • Replace IDataDomeContext and its Validate/Collect methods with one dedicated class per operation, such as ValidateLogin and CollectAccountUpdate.
  • Replace the shared DataDomeEvent builder with per-event payload builders.
  • Validate the payload at build time and throw a ValidationException on missing required fields.
  • Add a RequestMetadata object, passed alongside the request, to override the host, protocol, and forwarded client IP the SDK reads from the request (see Step 7).
  • Replace the response Status field with exceptions: validate calls still fail open and return Allow, while collect and feedback calls now throw ApiException, HttpException, or RequestTimeoutException.
  • Remove the Status and IP fields from the response model.
  • Remove asynchronous validate and collect calls. Wrap the synchronous Perform(...) call yourself if you need non-blocking behavior (see Step 6).
  • Add support for custom events with ValidateCustom and CollectCustom.
  • Add support for the feedback endpoint.
  • Add support for .NET 8.0.

Migration steps

Step 1: Upgrade to the latest package version

dotnet add package DataDome.AspNetCore.Fraud.SDK --version 2.0.0

Step 2: Update the client instantiation

Replace AddDataDome and DataDomeOptions with a Client built by Client.Builder(...) and registered as a singleton:

using DataDome.AspNetCore.Fraud.SDK;

var appBuilder = WebApplication.CreateBuilder(args);
appBuilder.Services.AddSingleton(_ => Client.Builder("your-api-key").Build());
var app = appBuilder.Build();
using DataDome.AspNetCore.Fraud.SDK;

var builder = WebApplication.CreateBuilder(args);

// Option 1: Passing in a reference to the configuration, for values in appsettings
builder.Services.AddDataDome(builder.Configuration);

// Option 2: Passing in the values directly, for environment variables
builder.Services.AddDataDome(o =>
{
    o.FraudAPIKey = "your-api-key";
});

Inject the concrete Client type instead of IDataDomeContext:

using DataDome.AspNetCore.Fraud.SDK;

public LoginController(Client client)
{
    _client = client;
}
using DataDome.AspNetCore.Fraud.SDK.Model.Shared;

public LoginController(SDK.IDataDomeContext dataDome)
{
    _dataDome = dataDome;
}

Step 3: Update event submission and payload construction

Replace IDataDomeContext.Validate/Collect with one dedicated class per operation. The table below maps every event to its v2 operations:

Eventv1 event classv2 validate operationv2 collect operation
LoginLoginEventValidateLoginCollectLogin
RegistrationRegistrationEventValidateRegistrationCollectRegistration
Account updateAccountUpdateEventValidateAccountUpdateCollectAccountUpdate
Password updatePasswordUpdateEventValidatePasswordUpdateCollectPasswordUpdate
Custom event(new in v2)ValidateCustomCollectCustom

The example below migrates a login event. Registration, account update, and password update follow the same pattern.

var builder = new LoginPayload.Builder()
    .Account(login)
    .Status(LoginPayloadStatus.Succeeded)
    .User(user)
    .Authentication(authentication)
    .Session(session);

var validate = new ValidateLogin(builder).Perform(client, ctx.Request, null);
if (validate?.Action == ResponseAction.Allow)
{
    return Results.Ok();
}
return Results.Problem("denied by Account Protect API", statusCode: 403);
var loginEvent = new LoginEvent(
    login,
    authentication: new Authentication { Mode = AuthenticationMode.Password, Type = AuthenticationType.Local },
    user: new User { Id = login },
    session: new Session { Id = Guid.NewGuid().ToString("D"), CreatedAt = DateTime.UtcNow });

var ddResponse = await _dataDome.Validate(Request, loginEvent);
if (ddResponse != null && ddResponse.Action == ResponseAction.Allow)
{
    // allowed
}

For a failed login, Perform is synchronous and wraps CollectLogin:

var builder = new LoginPayload.Builder()
    .Account(login)
    .Status(LoginPayloadStatus.Failed);
try
{
    new CollectLogin(builder).Perform(client, ctx.Request, null);
}
catch (Exception ex)
{
    Console.Error.WriteLine($"error during collection: {ex}");
}
await _dataDome.Collect(Request,
    new LoginEvent(login, LoginStatus.Failed, failReason: LoginFailReason.WrongPassword));

Step 4: Update your models

Replace the shared Address, User, and Authentication models with per-event types, and replace array fields with List<T>:

var userAddress = new UserAllOfAddress { Name = "Élysée Palace", CountryCode = "FR" };
var user = new LoginPayloadAllOfUser
{
    Id = "fake_user_id",
    PictureUrls = new List<string> { "https://example.com/profile.jpg" },
    ExternalUrls = new List<string> { "https://example.com" },
};
var user = new User
{
    Id = "fake_user_id",
    PictureUrls = new[] { "https://example.com/profile.jpg" },
    ExternalUrls = new[] { "https://example.com" },
};

This table maps the renamed model types:

v1v2
AddressUserAllOfAddress
UserLoginPayloadAllOfUser, RegistrationPayloadAllOfUser, AccountUpdatePayloadAllOfUser, or PasswordUpdatePayloadAllOfUser, depending on the event
AuthenticationLoginPayloadAllOfAuthentication, RegistrationPayloadAllOfAuthentication, or AccountUpdatePayloadAllOfAuthentication, depending on the event
Title = "mrs" (string)Title = UserTitle.Mrs (enum)
new CustomField("name", "value") { ... }new CustomField { Name = "name", Value = "value", ... }

Step 5: Update your response and error handling

Replace the response's ResponseAction property with Action, and remove reads of the removed Status and IP fields:

if (validate?.Action == ResponseAction.Allow)
if (ddResponse != null && ddResponse.ResponseAction == ResponseAction.Allow)

Wrap collect and feedback calls in a try/catch block: they now throw ApiException, HttpException, or RequestTimeoutException instead of returning a Status field. Validate calls still fail open and return Allow on error.

Step 6: Handle asynchronous calls yourself

Version 1's IDataDomeContext.Validate and .Collect returned Task<Response> and were always called with await. Version 2's Perform(client, request, requestMetadata) is synchronous: there's no PerformAsync.

If your application relies on non-blocking calls, wrap Perform(...) in a Task, for example with Task.Run(...):

var validate = await Task.Run(() => new ValidateLogin(builder).Perform(client, ctx.Request, null));
var ddResponse = await _dataDome.Validate(Request, loginEvent);

Step 7: Pass forwarded-proxy headers through RequestMetadata

Version 1 always read the host, protocol, and forwarded client IP directly from the incoming request, with no way to override them. If your application runs behind a reverse proxy or load balancer that doesn't already normalize these values before they reach your app (for example, via ASP.NET Core's Forwarded Headers Middleware), the SDK could end up validating the proxy's address instead of the client's.

Version 2 introduces RequestMetadata to fix this: pass it as the third argument to Perform(...), and each non-null property overrides the corresponding value the SDK would otherwise read from the request:

var metadata = new RequestMetadata()
    .WithHost(ctx.Request.Headers["X-Forwarded-Host"])
    .WithProtocol(ctx.Request.Headers["X-Forwarded-Proto"])
    .WithXForwardedForIp(ctx.Request.Headers["X-Forwarded-For"]);

var validate = new ValidateLogin(builder).Perform(client, ctx.Request, metadata);

There was no equivalent in version 1: the SDK always used request.Scheme and the Host/X-Forwarded-For headers as-is, so a reverse proxy that stripped or rewrote them would silently produce the wrong client IP or host.

Reference

Status and reason constants

v1v2
LoginStatus.Failed / .SucceededLoginPayloadStatus.Failed / .Succeeded
PasswordStatus.Succeeded / .FailedPasswordUpdatePayloadStatus.Succeeded / .Failed
Reason.UserUpdatePasswordUpdatePayloadReason.UserUpdate

Configuration changes

v1v2
AddDataDome(...) / DataDomeOptionsClient.Builder(key)
await _dataDome.Validate(...) / .Collect(...)Synchronous Perform(...). Wrap it in a Task yourself for non-blocking calls (see Step 6).
No override for the host, protocol, or forwarded client IPRequestMetadata (see Step 7)
Response Status and IP fieldsRemoved

Did this page help you?