top of page

Lleva tus Custom Connectors de Power Platform al siguiente nivel.

hace 15 minutos
6 min de lectura

Los Custom Connectors permiten exponer APIs y servicios externos de forma reutilizable dentro del ecosistema de Microsoft Power Platform. De esta forma, una integración puede ser consumida desde diferentes experiencias, como Power Apps, Power Automate o agentes de Copilot Studio, sin que cada solución tenga que implementar directamente la lógica de comunicación con la API.

Esto convierte al Custom Connector en una pieza de integración que no solo conecta sistemas, sino que también permite encapsular la complejidad técnica de una API y ofrecer una interfaz preparada para ser reutilizada por distintas soluciones y experiencias de usuario.

Pero ¿qué ocurre cuando la API con la que necesitamos trabajar no encaja exactamente con lo que Power Platform nos permite configurar de forma estándar?

Aquí es donde los Custom Connectors dejan de ser simplemente configuración y empiezan a convertirse en una herramienta de desarrollo.


Más allá de un conector estándar.

Un Custom Connector permite exponer una API de forma sencilla dentro de Power Platform, pero sus posibilidades no terminan en definir endpoints, parámetros y autenticación.

Podemos extender su comportamiento mediante:

  • Políticas, utilizando las plantillas disponibles en Power Platform.

  • Código personalizado en C#, para escenarios que requieren una transformación o lógica adicional.

Esta combinación permite adaptar el conector tanto a las necesidades de la API como a la experiencia que queremos ofrecer al usuario final.


Políticas: pequeñas modificaciones con mucho impacto.

Las políticas permiten modificar determinados comportamientos del conector sin necesidad de desarrollar código.


Entre las opciones disponibles encontramos, por ejemplo:


  • Establecer dinámicamente la URL del host.

  • Añadir o modificar encabezados HTTP.

  • Establecer propiedades o parámetros de consulta.

  • Enrutar solicitudes.

  • Transformar matrices y objetos.

  • Convertir cadenas delimitadas en matrices de objetos.



Un caso habitual es trabajar con diferentes hosts para distintos entornos. Podemos configurar el conector para que el usuario indique el host correspondiente al crear la conexión, evitando tener que crear conectores diferentes para desarrollo, sandbox y producción.


Otro escenario frecuente aparece cuando una API espera un formato concreto en sus encabezados. Por ejemplo, cuando necesita recibir una API Key o un Bearer token como:


apikey YOUR_API_KEY

En lugar de obligar al usuario a conocer ese formato, podemos utilizar una política que añada automáticamente el prefijo necesario.


El resultado es un conector mucho más sencillo de utilizar y más cercano a la experiencia que esperamos de un componente para un maker.


Cuando las políticas no son suficientes: código personalizado.

Hay escenarios en los que las plantillas disponibles no son suficientes.

Para estos casos, Power Platform permite incorporar código personalizado en C#, que nos da mayor control sobre las solicitudes y respuestas del conector.

Por ejemplo, podemos transformar la respuesta de una API antes de devolverla a Power Apps. Power Automate o Copilot Studio.

Esto resulta especialmente interesante cuando una API devuelve una estructura compleja, pero el usuario realmente necesita trabajar únicamente con una pequeña parte de esa información.

public class Script : ScriptBase
{
    public override async Task<HttpResponseMessage> ExecuteAsync()
    {
        // Check if the operation ID matches what is specified in the OpenAPI definition of the connector
        if (this.Context.OperationId == "Converter")
        {
            return await this.HandleForwardAndTransformOperation().ConfigureAwait(false);
        }

        // Handle an invalid operation ID
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
        response.Content = CreateJsonContent($"Unknown operation ID '{this.Context.OperationId}'");
        return response;
    }

    private async Task<HttpResponseMessage> HandleForwardAndTransformOperation()
    {
        // Use the context to forward/send an HTTP request
        HttpResponseMessage response =
            await this.Context.SendAsync(this.Context.Request, this.CancellationToken)
                .ConfigureAwait(false);

        // Do the transformation if the response was successful, otherwise return error responses as-is
        if (response.IsSuccessStatusCode)
        {
            var responseString =
                await response.Content.ReadAsStringAsync().ConfigureAwait(false);

            // Example case: response string is some JSON object
            var result = JObject.Parse(responseString);

            var newResult = new JObject
            {
                ["timestamp"] = result["info"]["timestamp"],
                ["date"] = result["date"],
                ["fromcurrency"] = result["query"]["from"],
                ["tocurrency"] = result["query"]["to"],
                ["fromamount"] = result["query"]["amount"],
                ["toamount"] = result["result"],
                ["exrate"] = result["info"]["rate"]
            };

            response.Content = CreateJsonContent(newResult.ToString());
        }

        return response;
    }
}

También podemos corregir problemas de la propia llamada.


El código personalizado también puede utilizarse para resolver problemas que aparecen durante la comunicación con determinadas APIs.

Un ejemplo es la codificación incorrecta de determinados caracteres en la URL.

Si el Custom Connector codifica una URL de una forma que impide que la API interprete correctamente la petición, podemos recuperar la URL generada, aplicar la transformación necesaria y continuar con la llamada.

public class Script : ScriptBase
{
    public override async Task<HttpResponseMessage> ExecuteAsync()
    {
        // get current request Uri
        var strRequestUri = this.Context.Request.RequestUri.AbsoluteUri;
        var strRequestUriDecode = HttpUtility.UrlDecode(strRequestUri);
        this.Context.Request.RequestUri = new Uri(strRequestUriDecode);

        HttpResponseMessage response =
            await this.Context.SendAsync(
                this.Context.Request,
                this.CancellationToken)
            .ConfigureAwait(false);

        return response;
    }
}

De nuevo, la complejidad queda encapsulada dentro del conector y no en la plataforma que se utilice.

¿Y si la autenticación que necesitamos no está soportada?

Uno de los escenarios más interesantes aparece cuando la API utiliza un método de autenticación que no está soportado directamente por Custom Connectors.

Un ejemplo es el flujo basado en Client Credentials.

En estos casos podemos combinar parámetros de conexión, políticas y código C# para construir un workaround.

El patrón consiste, simplificando, en:

Incorporar al conector los parámetros necesarios para la autenticación (Client Id, Client Secret y endpoint host).

Descargaremos los ficheros de nuestro conector y manualmente añadiremos en el apiProperties.json los paremetros de conexión.

{

  “properties”: {
    “connectionParameters”: {
      “token”: {
        “type”: “string”,
        “uiDefinition”: {
          “displayName”: “token”,
          “description”: “Token for the selected environment”,
          “tooltip”: “Provide the token”,
          “constraints”: {
            “required”: “true”
          }
        }
      },
      “authType”: {
        “type”: “string”,
        “allowedValues”: [
          {
            “value”: “none”
          }
        ],
        “uiDefinition”: {
          “displayName”: “Tipo de autenticacion”,
          “description”: “Tipo de autenticacion para conectarse a la API”,
          “tooltip”: “Tipo de autenticacion para conectarse a la API”,
          “constraints”: {
            “tabIndex”: 1,
            “required”: “true”,
            “allowedValues”: [
              {
                “text”: “none”,
                “value”: “anonymous”
              }
            ],
            “capability”: [
              “gateway”
            ]
          }
        }
      },
      “gateway”: {
        “type”: “gatewaySetting”,
        “gatewaySettings”: {
          “dataSourceType”: “CustomConnector”,
          “connectionDetails”: []
        },
        “uiDefinition”: {
          “constraints”: {
            “tabIndex”: 4,
            “required”: “true”,
            “capability”: [
              “gateway”
            ]
          }
        }
      }
    },
    “iconBrandColor”: “#007ee5”,
    “capabilities”: [
      “gateway”
    ],
    “policyTemplateInstances”: [
      {
        “templateId”: “setheader”,
        “title”: “Auth”,
        “parameters”: {
          “x-ms-apimTemplateParameter.name”: “Authorization”,
          “x-ms-apimTemplateParameter.value”: “apikey @connectionParameters(‘token’)”,
          “x-ms-apimTemplateParameter.existsAction”: “override”,
          “x-ms-apimTemplate-policySection”: “Request”
        }
      }
    ],
    “publisher”: “Mar Pedroche”
  }
}

Utilizar políticas para trasladar esos valores a los encabezados.

Como hemos visto en los ejemplos anteriores.


Añadir código personalizado que haga lo siguiente:
  1. Utilizar código personalizado para realizar la obtención del token.

  2. Eliminar los encabezados utilizados durante el proceso de autenticación.

  3. Añadir únicamente el encabezado Authorization con el token obtenido.

  4. Ejecutar finalmente la llamada contra la API.


public class Script : ScriptBase
{
    public override async Task<HttpResponseMessage> ExecuteAsync()
    {
        // Obtener el token desde el proveedor OAuth2 usando Client Credentials
        var accessToken = await GetAccessTokenAsync();

        // Agregar el token al encabezado Authorization
        Context.Request.Headers.Remove("authorization");
        Context.Request.Headers.Remove("ClientId");
        Context.Request.Headers.Remove("ClientSecret");

        Context.Request.Headers.TryAddWithoutValidation(
            "authorization",
            "Bearer " + accessToken);

        Context.Logger.LogInformation($"accessToken: {accessToken}");
        Context.Logger.LogInformation(
            $"url: {Context.Request.RequestUri?.ToString()}");

        // Enviar la solicitud original con el token
        var response = await Context.SendAsync(
                Context.Request,
                CancellationToken)
            .ConfigureAwait(false);

        return response;
    }

    private async Task<string> GetAccessTokenAsync()
    {
        string clientId = "";
        string clientSecret = "";

        // Obtener los valores desde los parámetros de conexión
        if (Context.Request.Headers.TryGetValues("ClientId", out var clientIdValue))
        {
            clientId = clientIdValue.FirstOrDefault()?.ToString();
        }

        if (Context.Request.Headers.TryGetValues("ClientSecret", out var secretIdValue))
        {
            clientSecret = secretIdValue.FirstOrDefault()?.ToString();
        }

        Context.Logger.LogInformation(
            $"clientID: {clientId}, clientSecret: {clientSecret}");

        string tokenEndpoint =
            "https://tokenexample.com/api/v1/oauth/token";

        string scope = "myscope";

        // Configurar la solicitud para obtener el token
        var tokenRequest = new HttpRequestMessage(
            HttpMethod.Post,
            tokenEndpoint)
        {
            Content = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>(
                    "grant_type",
                    "client_credentials"),

                new KeyValuePair<string, string>(
                    "client_id",
                    clientId),

                new KeyValuePair<string, string>(
                    "client_secret",
                    clientSecret),

                new KeyValuePair<string, string>(
                    "scope",
                    scope)
            })
        };

        // Enviar la solicitud al endpoint de token utilizando Context.SendAsync
        var tokenResponse = await Context.SendAsync(
                tokenRequest,
                CancellationToken)
            .ConfigureAwait(false);

        if (!tokenResponse.IsSuccessStatusCode)
        {
            throw new Exception(
                $"Error al obtener el token. Código de estado: {tokenResponse.StatusCode}, Razón: {tokenResponse.ReasonPhrase}");
        }

        // Leer y extraer el token manualmente desde la respuesta JSON
        var tokenResponseContent =
            await tokenResponse.Content.ReadAsStringAsync();

        return ExtractAccessTokenFromResponse(tokenResponseContent);
    }

    private string ExtractAccessTokenFromResponse(string responseContent)
    {
        // Extraer el valor del campo "access_token" usando manipulación de cadenas
        var tokenKey = "\"access_token\":\"";
        var startIndex = responseContent.IndexOf(tokenKey);

        if (startIndex == -1)
        {
            throw new Exception(
                "El campo 'access_token' no se encontró en la respuesta del token.");
        }

        startIndex += tokenKey.Length;

        var endIndex = responseContent.IndexOf("\"", startIndex);

        if (endIndex == -1)
        {
            throw new Exception(
                "El valor del campo 'access_token' no está bien formado.");
        }

        return responseContent.Substring(
            startIndex,
            endIndex - startIndex);
    }
}

De esta forma, conseguimos adaptar el comportamiento del Custom Connector a un escenario de autenticación que inicialmente no podía configurarse directamente.

Además, durante el desarrollo podemos utilizar Context.Logger.LogInformation para añadir trazas y consultar posteriormente los resultados desde los Code Logs del conector.

El objetivo no es "meter código por meter código"

La posibilidad de utilizar C# dentro de un Custom Connector no significa que debamos convertir cada integración en un desarrollo complejo. Además porque tiene limites de tiempo y de tamaño (5s y 1 MB).


El objetivo es precisamente el contrario: utilizar el código para encapsular la complejidad y ofrecer una experiencia simplificada al consumidor del conector.


Un buen Custom Connector debería ocultar detalles como:


  • Formatos específicos de autenticación.

  • Transformaciones de payload.

  • Estructuras complejas de respuesta.

  • Particularidades de una API.

  • Problemas de codificación.

  • Configuraciones específicas de cada entorno.


Custom Connectors como puente entre desarrollo y negocio.


Esta es, precisamente, una de las grandes ventajas de los Custom Connectors dentro de una estrategia de Power Platform.

El equipo técnico puede encargarse de la complejidad de la integración mientras que usuarios makers pueden consumirlos desde distintos origenes como Copilot Studio, Power Automate y Power Apps.

Porque llevar Power Platform al siguiente nivel no siempre significa escribir más código.

A veces significa escribir el código necesario para hacer componentes reutilizables por cualquier usuario.

Comentarios


Ya no es posible comentar esta entrada. Contacta al propietario del sitio para obtener más información.

Déjame algún comentario

&iexcl;Gracias por contactar!

© 2023 by Power User 365 blog

bottom of page