From f288420d741b8a1ebed3d609a76304ac15e244bc Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Wed, 21 Oct 2026 14:00:24 +0000 Subject: [PATCH 1/2] feat: add create user list sample --- samples/CreateUserList.cs | 274 ++++++++++++++++++++++++++++++++++++++ samples/Program.cs | 7 +- 2 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 samples/CreateUserList.cs diff --git a/samples/CreateUserList.cs b/samples/CreateUserList.cs new file mode 100644 index 0000000..8902dc5 --- /dev/null +++ b/samples/CreateUserList.cs @@ -0,0 +1,274 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; +using CommandLine; +using Google.Ads.DataManager.V1; +using Google.Api.Gax.Grpc; +using Google.Protobuf.Reflection; +using Google.Protobuf.WellKnownTypes; +using static Google.Ads.DataManager.V1.IngestedUserListInfo.Types; +using static Google.Ads.DataManager.V1.MobileIdInfo.Types; +using static Google.Ads.DataManager.V1.ProductAccount.Types; + +namespace Google.Ads.DataManager.Samples +{ + // + // Sample of creating a in an operating account. + // + public class CreateUserList + { + [Verb("create-user-list", HelpText = "Creates a UserList in an operating account.")] + public class Options + { + [Option( + "operatingAccountType", + Required = true, + HelpText = "Account type of the operating account" + )] + public AccountType OperatingAccountType { get; set; } + + [Option( + "operatingAccountId", + Required = true, + HelpText = "ID of the operating account" + )] + public string OperatingAccountId { get; set; } = null!; + + [Option( + "uploadKeyType", + Required = false, + Default = UploadKeyType.ContactId, + HelpText = "Upload key type of the user list (ContactId, UserId, or MobileId). Defaults to ContactId." + )] + public UploadKeyType UploadKeyType { get; set; } + + [Option( + "loginAccountType", + Required = false, + HelpText = "Account type of the login account" + )] + public AccountType? LoginAccountType { get; set; } + + [Option("loginAccountId", Required = false, HelpText = "ID of the login account")] + public string? LoginAccountId { get; set; } + + [Option( + "linkedAccountType", + Required = false, + HelpText = "Account type of the linked account" + )] + public AccountType? LinkedAccountType { get; set; } + + [Option("linkedAccountId", Required = false, HelpText = "ID of the linked account")] + public string? LinkedAccountId { get; set; } + + [Option( + "keySpace", + Required = false, + HelpText = "Key space of mobile IDs (Android or Ios). Required when --uploadKeyType is MobileId." + )] + public KeySpace? KeySpace { get; set; } + + [Option( + "appId", + Required = false, + HelpText = "String that uniquely identifies a mobile application from which the data was collected (for example, 'com.labpixies.flood' for Android or '476943146' for iOS). Required when --uploadKeyType is MobileId." + )] + public string? AppId { get; set; } + + [Option( + "validateOnly", + Default = true, + HelpText = "Whether to enable validateOnly on the request" + )] + public bool ValidateOnly { get; set; } + } + + public void Run(Options options) + { + RunExample( + options.OperatingAccountType, + options.OperatingAccountId, + options.ValidateOnly, + options.UploadKeyType, + options.LoginAccountType, + options.LoginAccountId, + options.LinkedAccountType, + options.LinkedAccountId, + options.KeySpace, + options.AppId + ); + } + + // [START create-user-list] + // Runs the example. + private void RunExample( + AccountType operatingAccountType, + string operatingAccountId, + bool validateOnly, + UploadKeyType uploadKeyType = UploadKeyType.ContactId, + AccountType? loginAccountType = null, + string? loginAccountId = null, + AccountType? linkedAccountType = null, + string? linkedAccountId = null, + KeySpace? keySpace = null, + string? appId = null + ) + { + if (loginAccountId == null ^ loginAccountType == null) + { + throw new ArgumentException( + "Must specify either both or neither of login account ID and login account " + + "type" + ); + } + if (linkedAccountId == null ^ linkedAccountType == null) + { + throw new ArgumentException( + "Must specify either both or neither of linked account ID and linked account " + + "type" + ); + } + if (uploadKeyType == UploadKeyType.MobileId) + { + if (!keySpace.HasValue || string.IsNullOrEmpty(appId)) + { + throw new ArgumentException( + "Must specify both keySpace and appId when uploadKeyType is MobileId" + ); + } + } + else if (keySpace.HasValue || !string.IsNullOrEmpty(appId)) + { + throw new ArgumentException( + "keySpace and appId are only supported when uploadKeyType is MobileId" + ); + } + + // Optional: Sets headers to specify the login-account and linked-account if your + // credentials aren't for a user in the operating account (for example, when + // authenticating through a Google Ads manager account or Data Partner account). + CallSettings? callSettings = null; + if (loginAccountType.HasValue && loginAccountId != null) + { + callSettings = callSettings.WithHeader( + "login-account", + // Uses the AccountName utility to construct the login-account resource name. + AccountName.Format(GetEnumOriginalName(loginAccountType.Value), loginAccountId) + ); + } + if (linkedAccountType.HasValue && linkedAccountId != null) + { + callSettings = callSettings.WithHeader( + "linked-account", + // Uses the AccountName utility to construct the linked-account resource name. + AccountName.Format( + GetEnumOriginalName(linkedAccountType.Value), + linkedAccountId + ) + ); + } + + // Configures the Customer Match user list. + // For more details on constructing a UserList, see: + // https://developers.google.com/data-manager/api/devguides/audiences/google-ads/customer-match/create-audience + var ingestedUserListInfo = new IngestedUserListInfo + { + // Sets to a list containing a single UploadKeyType. + UploadKeyTypes = { uploadKeyType }, + }; + + switch (uploadKeyType) + { + case UploadKeyType.ContactId: + // Use CONTACT_ID for hashed email addresses, phone numbers, or physical + // addresses. + ingestedUserListInfo.ContactIdInfo = new ContactIdInfo + { + DataSourceType = DataSourceType.FirstParty, + }; + break; + case UploadKeyType.UserId: + // Use USER_ID for first-party user IDs assigned to logged-in users. + // Note: Display & Video 360 does not support USER_ID lists. + ingestedUserListInfo.UserIdInfo = new UserIdInfo + { + DataSourceType = DataSourceType.FirstParty, + }; + break; + case UploadKeyType.MobileId: + // Use MOBILE_ID for mobile advertising IDs. + ingestedUserListInfo.MobileIdInfo = new MobileIdInfo + { + DataSourceType = DataSourceType.FirstParty, + KeySpace = keySpace!.Value, + AppId = appId, + }; + break; + default: + throw new ArgumentException($"Unsupported upload key type: {uploadKeyType}"); + } + + string uploadKeyTypeName = GetEnumOriginalName(uploadKeyType); + var userList = new UserList + { + // Required. The display name must be unique among all lists owned by the account. + DisplayName = + $"Customer Match ({uploadKeyTypeName}) - {DateTime.Now:yyyy-MM-dd HH:mm:ss}", + Description = $"Customer Match audience for {uploadKeyTypeName}", + // Optional. The duration an audience member remains in the list after being added. + // Must correspond to whole days (defaults to maximum if not set). + MembershipDuration = Duration.FromTimeSpan(TimeSpan.FromDays(30)), + // Required for a Customer Match audience. + IngestedUserListInfo = ingestedUserListInfo, + }; + + // Creates a client for UserListService. + UserListServiceClient userListServiceClient = UserListServiceClient.Create(); + + // Builds the request targeting the parent operating account. + var request = new CreateUserListRequest + { + Parent = AccountName.Format( + GetEnumOriginalName(operatingAccountType), + operatingAccountId + ), + UserList = userList, + // Sets validate_only. If true, then the Data Manager API only validates the + // request but doesn't apply changes. + ValidateOnly = validateOnly, + }; + + // Sends the request with any configured header settings. + UserList response = userListServiceClient.CreateUserList(request, callSettings); + + // Logs the created user list. When validate_only is false, response.Id contains the + // created user list ID, which you pass as the ProductDestinationId in subsequent + // ingest or remove requests. + Console.WriteLine($"Response:\n{response}"); + } + + // Returns the original proto enum name (for example, "GOOGLE_ADS" for + // AccountType.GoogleAds). + private static string GetEnumOriginalName(T enumValue) + where T : struct, System.Enum => + typeof(T) + .GetField(enumValue.ToString()) + ?.GetCustomAttribute() + ?.Name + ?? enumValue.ToString(); + // [END create-user-list] + } +} diff --git a/samples/Program.cs b/samples/Program.cs index 8e8dcc2..a62949d 100644 --- a/samples/Program.cs +++ b/samples/Program.cs @@ -21,7 +21,12 @@ class Program static void Main(string[] args) { Parser - .Default.ParseArguments(args) + .Default.ParseArguments< + CreateUserList.Options, + IngestAudienceMembers.Options, + IngestEvents.Options + >(args) + .WithParsed(o => new CreateUserList().Run(o)) .WithParsed(o => new IngestAudienceMembers().Run(o)) .WithParsed(o => new IngestEvents().Run(o)); } From 13524beeba47a868280fc1d33b1b939bc93e08c5 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Wed, 21 Oct 2026 14:38:24 +0000 Subject: [PATCH 2/2] chore: update XML doc summary blocks to use three slashes --- samples/CreateUserList.cs | 6 +++--- samples/IngestAudienceMembers.cs | 12 ++++++------ samples/IngestEvents.cs | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/samples/CreateUserList.cs b/samples/CreateUserList.cs index 8902dc5..f94f9a4 100644 --- a/samples/CreateUserList.cs +++ b/samples/CreateUserList.cs @@ -24,9 +24,9 @@ namespace Google.Ads.DataManager.Samples { - // - // Sample of creating a in an operating account. - // + /// + /// Sample of creating a in an operating account. + /// public class CreateUserList { [Verb("create-user-list", HelpText = "Creates a UserList in an operating account.")] diff --git a/samples/IngestAudienceMembers.cs b/samples/IngestAudienceMembers.cs index 9a5608c..352e07c 100644 --- a/samples/IngestAudienceMembers.cs +++ b/samples/IngestAudienceMembers.cs @@ -22,12 +22,12 @@ namespace Google.Ads.DataManager.Samples { - // - // Sends a without using encryption. - // - // User data is read from a data file. See the audience_members_1.json file in the - // sampledata directory for an example. - // + /// + /// Sends a without using encryption. + /// + /// User data is read from a data file. See the audience_members_1.json file in the + /// sampledata directory for an example. + /// public class IngestAudienceMembers { private static readonly int MaxMembersPerRequest = 10_000; diff --git a/samples/IngestEvents.cs b/samples/IngestEvents.cs index 173535d..3763196 100644 --- a/samples/IngestEvents.cs +++ b/samples/IngestEvents.cs @@ -22,12 +22,12 @@ namespace Google.Ads.DataManager.Samples { - // - // Sends an without using encryption. - // - // Event data is read from a data file. See the events_1.json file in the - // sampledata directory for an example. - // + /// + /// Sends an without using encryption. + /// + /// Event data is read from a data file. See the events_1.json file in the + /// sampledata directory for an example. + /// public class IngestEvents { private static readonly int MaxEventsPerRequest = 2_000;