Compartir a través de


Tutorial: Aplicación web de BING Ads API en C#

En este ejemplo, la aplicación web de C# solicita el consentimiento del usuario a través de las credenciales que proporcione y, a continuación, obtiene las cuentas a las que el usuario autenticado puede acceder.

Primero debe registrar una aplicación y tomar nota del identificador de cliente (identificador de aplicación registrado), el secreto de cliente (contraseña registrada) y el URI de redirección. Para obtener más información sobre cómo registrar una aplicación y el flujo de concesión de código de autorización, consulte Autenticación con OAuth.

También necesitará el token de desarrollador de producción. Puede crear el ejemplo paso a paso como se describe a continuación o descargar más ejemplos de GitHub.

Sugerencia

En este ejemplo se hace referencia a los pasos de Creación de una aplicación web de ASP.NET Framework en Azure. Para más información sobre la implementación de aplicaciones web en Azure, consulte la documentación de Azure.

Tutorial de código

  1. Abra el entorno de desarrollo de Visual Studio Community 2017. Si ya ha instalado Visual Studio, agregue las cargas de trabajo de desarrollo ASP.NET y web y desarrollo de Azure en Visual Studio haciendo clic en Herramientas>Obtener herramientas y características.

  2. Para crear un proyecto, seleccione Archivo > nuevo > proyecto. En la ventana Nuevo proyecto , elija .NET Framework 4.7.1 en la lista desplegable y, a continuación, seleccione Visual C# > Web > ASP.NET Web Application (.NET Framework). Asigne al proyecto el nombre BingAdsWebApp y haga clic en Aceptar.

  3. Puede implementar cualquier tipo de aplicación web de ASP.NET en Azure. Para este inicio rápido, seleccione la plantilla MVC , asegúrese de que la autenticación está establecida en Sin autenticación y haga clic en Aceptar.

  4. Instale el SDK a través de NuGet para BingAdsWebApp. Para obtener más información sobre las dependencias, consulte Instalación del SDK. Haga clic en Herramientas ->Administrador de paquetes NuGet ->Consola del Administrador de paquetes. En el símbolo del sistema, escriba estos comandos para instalar los paquetes de uno en uno: Install-Package Microsoft.BingAds.SDK, Install-Package System.ServiceModel.Primitives -Version 4.4.1, Install-Package System.ServiceModel.Http -Version 4.4.1y Install-Package System.Configuration.ConfigurationManager -Version 4.4.1.

  5. Abra el archivo Web.config y reemplace su contenido por el siguiente bloque de código. Edite BingAdsEnvironment para pasar del espacio aislado a la producción y establezca el token de desarrollador de producción según sea necesario. Debe editar ClientId, ClientSecret y RedirectUri con los valores correspondientes De id. de aplicación, Secreto de aplicación y Dirección URL de redireccionamiento que se aprovisionaron al registrar la aplicación.

Nota:

Si tiene previsto realizar la implementación en localhost antes de entrar en directo, asegúrese de registrar también la dirección URL y el puerto SSL locales, por ejemplo, https://localhost:44383/. En ese caso, también debe establecer SSL Habilitado en True en la ventana de propiedades del proyecto BingAdsWebApp y, a continuación, puede copiar el puerto localhost desde la misma ventana de propiedades.

  <?xml version="1.0" encoding="utf-8"?>
  <!--
      For more information on how to configure your ASP.NET application, please visit
      https://go.microsoft.com/fwlink/?LinkId=301880
      -->
  <configuration>
      <configSections>
      <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
          <section name="BingAdsWebApp.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
      </sectionGroup>
      </configSections>
      <appSettings>
      <!-- To use the production environment, set this value to "Production". -->
      <add key="BingAdsEnvironment" value="Sandbox"/>
      <add key="webpages:Version" value="3.0.0.0" />
      <add key="webpages:Enabled" value="false" />
      <add key="ClientValidationEnabled" value="true" />
      <add key="UnobtrusiveJavaScriptEnabled" value="true" />
      </appSettings>
      <system.web>
      <compilation debug="true" targetFramework="4.7.1" />
      <httpRuntime targetFramework="4.7.1" />
      <httpModules>
          <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
      </httpModules>
      </system.web>
      <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
          <dependentAssembly>
          <assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
          <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
          </dependentAssembly>
          <dependentAssembly>
          <assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35" />
          <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
          </dependentAssembly>
          <dependentAssembly>
          <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" />
          <bindingRedirect oldVersion="0.0.0.0-1.5.2.14234" newVersion="1.5.2.14234" />
          </dependentAssembly>
          <dependentAssembly>
          <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
          <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
          </dependentAssembly>
          <dependentAssembly>
          <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
          <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
          </dependentAssembly>
          <dependentAssembly>
          <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
          <bindingRedirect oldVersion="1.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
          </dependentAssembly>
      </assemblyBinding>
      </runtime>
      <system.webServer>
      <validation validateIntegratedModeConfiguration="false" />
      <modules>
          <remove name="ApplicationInsightsWebTracking" />
          <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler" />
      </modules>
      </system.webServer>
      <system.codedom>
      <compilers>
          <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
          <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
      </compilers>
      </system.codedom>
      <applicationSettings>
      <BingAdsWebApp.Properties.Settings>
          <setting name="RefreshToken" serializeAs="String">
          <value />
          </setting>
          <setting name="ClientId" serializeAs="String">
          <value>ClientIdGoesHere</value>
          </setting>
          <setting name="ClientSecret" serializeAs="String">
          <value>ClientSecretGoesHere</value>
          </setting>
          <setting name="RedirectionUri" serializeAs="String">
          <value>RedirectionUriGoesHere</value>
          </setting>
          <setting name="DeveloperToken" serializeAs="String">
          <value>DeveloperTokenGoesHere</value>
          </setting>
      </BingAdsWebApp.Properties.Settings>
      </applicationSettings>
  </configuration>
  1. Cree un archivo de configuración. En la vista de proyecto de BingAdsWebApp, haga clic con el botón derecho en Propiedades y haga clic en Abrir. Haga clic en Configuración y, a continuación, haga clic en el texto El proyecto no contiene un archivo de configuración predeterminado. Haga clic aquí para crear uno. Se agregarán automáticamente nuevos valores de Web.config.

  2. En la carpeta Views ->Home del proyecto BingAdsWebApp, abra el archivo Index.cshtml y reemplace su contenido por el siguiente bloque de código. Esto define la vista de página web que muestra los resultados de las llamadas de servicio que se escribirán más adelante.

    @{
        ViewBag.Title = "Index";
    }
    
    <h2>Index</h2>
    
    <h4>Accounts</h4>
    <p>@Html.Raw(@ViewBag.Accounts)</p>
    
    <p><b style="color: red">@ViewBag.Errors</b></p>
    
  3. En la carpeta Controllers del proyecto BingAdsWebApp, abra el archivo HomeController.cs y reemplace su contenido por el siguiente bloque de código. Esto define las llamadas de servicio que determinarán qué resultados se muestran en la vista que se definió anteriormente.

    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Linq;
    using System.Net.Http;
    using System.ServiceModel;
    using System.Threading.Tasks;
    using System.Web.Mvc;
    using BingAdsWebApp.Properties;
    using Microsoft.BingAds;
    using Microsoft.BingAds.V13.CustomerManagement;
    
    namespace BingAdsWebApp.Controllers
    {
        public class HomeController : Controller
        {
            private static AuthorizationData _authorizationData;
            private static ServiceClient<ICustomerManagementService> _customerManagementService;
            private static string ClientState = "ClientStateGoesHere";
            private static string _output = "";
    
            /// <summary>
            /// Controls the contents displayed at Index.cshtml.
            /// </summary>
            public async Task<ActionResult> Index()
            {
                try
                {
                    // If there is already an authenticated Microsoft account during this HTTP session, 
                    // go ahead and call Bing Ads API service operations.
    
                    if (Session["auth"] != null)
                    {
                        return await SetAuthorizationDataAsync((OAuthWebAuthCodeGrant)Session["auth"]);
                    }
    
                    // Prepare the OAuth object for use with the authorization code grant flow. 
    
                    var apiEnvironment =
                        ConfigurationManager.AppSettings["BingAdsEnvironment"] == ApiEnvironment.Sandbox.ToString() ?
                        ApiEnvironment.Sandbox : ApiEnvironment.Production;
                    var oAuthWebAuthCodeGrant = new OAuthWebAuthCodeGrant(
                        Settings.Default["ClientId"].ToString(),
                        Settings.Default["ClientSecret"].ToString(), 
                        new Uri(Settings.Default["RedirectionUri"].ToString()),
                        apiEnvironment);
    
                    // It is recommended that you specify a non guessable 'state' request parameter to help prevent
                    // cross site request forgery (CSRF). 
                    oAuthWebAuthCodeGrant.State = ClientState;
    
                    // When calling Bing Ads API service operations with ServiceClient or BulkServiceManager, each will refresh your access token 
                    // automatically if they detect the AuthenticationTokenExpired (109) error code. 
                    // As a best practice you should always use the most recent provided refresh token.
                    // Save the refresh token whenever new OAuth tokens are received by subscribing to the NewOAuthTokensReceived event handler. 
    
                    oAuthWebAuthCodeGrant.NewOAuthTokensReceived +=
                        (sender, args) => SaveRefreshToken(args.NewRefreshToken);
    
                    // If a refresh token is already present, use it to request new access and refresh tokens.
    
                    if (RefreshTokenExists())
                    {
                        await oAuthWebAuthCodeGrant.RequestAccessAndRefreshTokensAsync(GetRefreshToken());
    
                        // Save the authentication object in a session for future requests.
                        Session["auth"] = oAuthWebAuthCodeGrant;
    
                        return await SetAuthorizationDataAsync((OAuthWebAuthCodeGrant)Session["auth"]);
                    }
    
                    // If the current HTTP request is a callback from the Microsoft Account authorization server,
                    // use the current request url containing authorization code to request new access and refresh tokens
                    if (Request["code"] != null)
                    {
                        if (oAuthWebAuthCodeGrant.State != ClientState)
                            throw new HttpRequestException("The OAuth response state does not match the client request state.");
    
                        await oAuthWebAuthCodeGrant.RequestAccessAndRefreshTokensAsync(Request.Url);
    
                        // Save the authentication object in a session for future requests. 
                        Session["auth"] = oAuthWebAuthCodeGrant;
    
                        return await SetAuthorizationDataAsync((OAuthWebAuthCodeGrant)Session["auth"]);
                    }
    
                    SaveRefreshToken(oAuthWebAuthCodeGrant.OAuthTokens?.RefreshToken);
    
                    // If there is no refresh token saved and no callback from the authorization server, 
                    // then connect to the authorization server and request user consent. 
                    return Redirect(oAuthWebAuthCodeGrant.GetAuthorizationEndpoint().ToString());
                }
                // Catch authentication exceptions
                catch (OAuthTokenRequestException ex)
                {
                    ViewBag.Errors = (string.Format("Couldn't get OAuth tokens. Error: {0}. Description: {1}", 
                        ex.Details.Error, ex.Details.Description));
                    return View();
                }
                // Catch Customer Management service exceptions
                catch (FaultException<Microsoft.BingAds.V13.CustomerManagement.AdApiFaultDetail> ex)
                {
                    ViewBag.Errors = (string.Join("; ", ex.Detail.Errors.Select(
                        error => string.Format("{0}: {1}", error.Code, error.Message))));
                    return View();
                }
                catch (FaultException<Microsoft.BingAds.V13.CustomerManagement.ApiFault> ex)
                {
                    ViewBag.Errors = (string.Join("; ", ex.Detail.OperationErrors.Select(
                        error => string.Format("{0}: {1}", error.Code, error.Message))));
                    return View();
                }            
                catch (Exception ex)
                {
                    ViewBag.Errors = ex.Message;
                    return View();
                }
            }
    
            /// <summary>
            /// Adds a campaign to an account of the current authenticated user. 
            /// </summary>
            private async Task<ActionResult> SetAuthorizationDataAsync(Authentication authentication)
            {
                _authorizationData = new AuthorizationData
                {
                    Authentication = authentication,
                    DeveloperToken = Settings.Default["DeveloperToken"].ToString()
                };
    
                _customerManagementService = new ServiceClient<ICustomerManagementService>(_authorizationData);
    
                var getUserRequest = new GetUserRequest
                {
                    UserId = null
                };
    
                var getUserResponse = 
                    (await _customerManagementService.CallAsync((s, r) => s.GetUserAsync(r), getUserRequest));
                var user = getUserResponse.User;
    
                var predicate = new Predicate
                {
                    Field = "UserId",
                    Operator = PredicateOperator.Equals,
                    Value = user.Id.ToString()
                };
    
                var paging = new Paging
                {
                    Index = 0,
                    Size = 10
                };
    
                var searchAccountsRequest = new SearchAccountsRequest
                {
                    Ordering = null,
                    PageInfo = paging,
                    Predicates = new[] { predicate }
                };
    
                var searchAccountsResponse =
                    (await _customerManagementService.CallAsync((s, r) => s.SearchAccountsAsync(r), searchAccountsRequest));
    
                var accounts = searchAccountsResponse.Accounts.ToArray();
                if (accounts.Length <= 0) return View();
    
                _authorizationData.AccountId = (long)accounts[0].Id;
                _authorizationData.CustomerId = (int)accounts[0].ParentCustomerId;
    
                OutputArrayOfAdvertiserAccount(accounts);
    
                ViewBag.Accounts = _output;
                _output = null;
    
                return View();
            }
    
            /// <summary>
            /// Saves the refresh token
            /// </summary>
            /// <param name="refreshToken">The refresh token to save</param>
            private static void SaveRefreshToken(string refreshToken)
            {
                Settings.Default["RefreshToken"] = refreshToken;
                Settings.Default.Save();
            }
    
            /// <summary>
            /// Deletes the contents in the refresh token file
            /// </summary>
            private static void DeleteRefreshToken()
            {
                Settings.Default["RefreshToken"] = "";
                Settings.Default.Save();
            }
    
            /// <summary>
            /// Determines whether the global refresh token exists.
            /// </summary>
            /// <returns>Returns true if the global refresh token exists.</returns>
            private bool RefreshTokenExists()
            {
                return Settings.Default["RefreshToken"] != null 
                    && Settings.Default["RefreshToken"].ToString().Length > 0;
            }
    
            /// <summary>
            /// Gets the global refresh token.
            /// </summary>
            /// <returns>The global refresh token.</returns>
            private string GetRefreshToken()
            {
                return Settings.Default["RefreshToken"].ToString();
            }
    
            #region OutputHelpers
    
            /**
             * You can extend the app with example output helpers at:
             * https://github.com/BingAds/BingAds-dotNet-SDK/tree/main/examples/BingAdsExamples/BingAdsExamplesLibrary/v13
             * 
             * AdInsightExampleHelper.cs
             * BulkExampleHelper.cs
             * CampaignManagementExampleHelper.cs
             * CustomerBillingExampleHelper.cs
             * CustomerManagementExampleHelper.cs
             * ReportingExampleHelper.cs
             **/
    
            private static void OutputArrayOfAdvertiserAccount(IList<AdvertiserAccount> dataObjects)
            {
                if (null != dataObjects)
                {
                    foreach (var dataObject in dataObjects)
                    {
                        OutputAdvertiserAccount(dataObject);
                        OutputStatusMessage("\n");
                    }
                }
            }
    
            private static void OutputAdvertiserAccount(AdvertiserAccount dataObject)
            {
                if (null != dataObject)
                {
                    OutputStatusMessage(string.Format("BillToCustomerId: {0}", dataObject.BillToCustomerId));
                    OutputStatusMessage(string.Format("CurrencyCode: {0}", dataObject.CurrencyCode));
                    OutputStatusMessage(string.Format("AccountFinancialStatus: {0}", dataObject.AccountFinancialStatus));
                    OutputStatusMessage(string.Format("Id: {0}", dataObject.Id));
                    OutputStatusMessage(string.Format("Language: {0}", dataObject.Language));
                    OutputStatusMessage(string.Format("LastModifiedByUserId: {0}", dataObject.LastModifiedByUserId));
                    OutputStatusMessage(string.Format("LastModifiedTime: {0}", dataObject.LastModifiedTime));
                    OutputStatusMessage(string.Format("Name: {0}", dataObject.Name));
                    OutputStatusMessage(string.Format("Number: {0}", dataObject.Number));
                    OutputStatusMessage(string.Format("ParentCustomerId: {0}", dataObject.ParentCustomerId));
                    OutputStatusMessage(string.Format("PaymentMethodId: {0}", dataObject.PaymentMethodId));
                    OutputStatusMessage(string.Format("PaymentMethodType: {0}", dataObject.PaymentMethodType));
                    OutputStatusMessage(string.Format("PrimaryUserId: {0}", dataObject.PrimaryUserId));
                    OutputStatusMessage(string.Format("AccountLifeCycleStatus: {0}", dataObject.AccountLifeCycleStatus));
                    OutputStatusMessage(string.Format("TimeStamp: {0}", dataObject.TimeStamp));
                    OutputStatusMessage(string.Format("TimeZone: {0}", dataObject.TimeZone));
                    OutputStatusMessage(string.Format("PauseReason: {0}", dataObject.PauseReason));
                    OutputArrayOfKeyValuePairOfstringstring(dataObject.ForwardCompatibilityMap);
                    OutputArrayOfCustomerInfo(dataObject.LinkedAgencies);
                    OutputStatusMessage(string.Format("SalesHouseCustomerId: {0}", dataObject.SalesHouseCustomerId));
                    OutputArrayOfKeyValuePairOfstringstring(dataObject.TaxInformation);
                    OutputStatusMessage(string.Format("BackUpPaymentInstrumentId: {0}", dataObject.BackUpPaymentInstrumentId));
                    OutputStatusMessage(string.Format("BillingThresholdAmount: {0}", dataObject.BillingThresholdAmount));
                    OutputAddress(dataObject.BusinessAddress);
                    OutputStatusMessage(string.Format("AutoTagType: {0}", dataObject.AutoTagType));
                    OutputStatusMessage(string.Format("SoldToPaymentInstrumentId: {0}", dataObject.SoldToPaymentInstrumentId));
                }
            }
    
            private static void OutputAddress(Address dataObject)
            {
                if (null != dataObject)
                {
                    OutputStatusMessage(string.Format("City: {0}", dataObject.City));
                    OutputStatusMessage(string.Format("CountryCode: {0}", dataObject.CountryCode));
                    OutputStatusMessage(string.Format("Id: {0}", dataObject.Id));
                    OutputStatusMessage(string.Format("Line1: {0}", dataObject.Line1));
                    OutputStatusMessage(string.Format("Line2: {0}", dataObject.Line2));
                    OutputStatusMessage(string.Format("Line3: {0}", dataObject.Line3));
                    OutputStatusMessage(string.Format("Line4: {0}", dataObject.Line4));
                    OutputStatusMessage(string.Format("PostalCode: {0}", dataObject.PostalCode));
                    OutputStatusMessage(string.Format("StateOrProvince: {0}", dataObject.StateOrProvince));
                    OutputStatusMessage(string.Format("TimeStamp: {0}", dataObject.TimeStamp));
                    OutputStatusMessage(string.Format("BusinessName: {0}", dataObject.BusinessName));
                }
            }
    
            private static void OutputArrayOfKeyValuePairOfstringstring(IList<KeyValuePair<string, string>> dataObjects)
            {
                if (null != dataObjects)
                {
                    foreach (var dataObject in dataObjects)
                    {
                        OutputKeyValuePairOfstringstring(dataObject);
                    }
                }
            }
    
            private static void OutputKeyValuePairOfstringstring(KeyValuePair<string, string> dataObject)
            {
                if (null != dataObject.Key)
                {
                    OutputStatusMessage(string.Format("key: {0}", dataObject.Key));
                    OutputStatusMessage(string.Format("value: {0}", dataObject.Value));
                }
            }
    
            private static void OutputCustomerInfo(CustomerInfo dataObject)
            {
                if (null != dataObject)
                {
                    OutputStatusMessage(string.Format("Id: {0}", dataObject.Id));
                    OutputStatusMessage(string.Format("Name: {0}", dataObject.Name));
                }
            }
    
            private static void OutputArrayOfCustomerInfo(IList<CustomerInfo> dataObjects)
            {
                if (null != dataObjects)
                {
                    foreach (var dataObject in dataObjects)
                    {
                        OutputCustomerInfo(dataObject);
                        OutputStatusMessage("\n");
                    }
                }
            }
    
            private static void OutputStatusMessage(String msg)
            {
                _output += (msg + "<br/>");
            }
    
            #endregion OutputHelpers
        }
    }
    
  4. Para implementar la aplicación web en la máquina local, primero debe establecer SSL habilitado en True en la ventana de propiedades del proyecto BingAdsWebApp. Si aún no lo ha hecho, la dirección URL de redireccionamiento de la aplicación registrada debe incluir la dirección URL y el puerto SSL, por ejemplo, https://localhost:44383/. En el menú, seleccione Depurar > inicio sin depuración para ejecutar la aplicación web localmente.

  5. Para implementar la aplicación web en directo mediante la Azure App Service consulte las instrucciones de suscripción e implementación de la documentación de Azure, por ejemplo, Creación de una aplicación web de ASP.NET Framework en Azure.

Configuración del espacio aislado

Para usar el espacio aislado, establezca la clave BingAdsEnvironment en Espacio aislado dentro del <nodo appSettings> del archivo deWeb.config raíz del proyecto.

<add key="BingAdsEnvironment" value ="Sandbox"/>

También puede establecer el entorno para cada ServiceClient individualmente de la siguiente manera.

_customerService = new ServiceClient<ICustomerManagementService>(_authorizationData, ApiEnvironment.Sandbox);

Tanto si establece el entorno ServiceClient globalmente como individualmente, por separado también tendrá que establecer el entorno de OAuth en espacio aislado.

var oAuthWebAuthCodeGrant = new OAuthWebAuthCodeGrant(ClientId, ClientSecret, new Uri(RedirectionUri), ApiEnvironment.Sandbox);

Consulta también

Espacio aislado
Ejemplos de código de api de Bing Ads
Direcciones del servicio web de la API de Bing Ads