Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=aa81b824-9dbc-4b19-a2ea-e026f56b3f14

ASP.NET: describiendo una API con OpenAPI y Swagger

Quien más y quien menos en este mundo del software, alguna vez ha tenido que escribir una biblioteca de clases (mal llamada librería) o una API REST. O, al menos, ha tenido que modificarla. Y como profesionales que somos, siempre documentamos y explicamos el funcionamiento para que los consumidores puedan usarla fácilmente ¿verdad? 😜

Aunque me gustaría que la respuesta fuese que siempre se documenta bien, basta con que piense en mis propios proyectos pasados para saber que no es así. ¿Y qué nos aporta esa documentación? Pues si estamos hablando de una biblioteca o un paquete NuGet (que es una biblioteca, al fin y al cabo), la diferencia salta a simple vista:

La imagen muestra el intellisense de un método que no tiene documentación, por lo que no se ve nada más que el método

La imagen muestra el intellisense de un método que sí tiene documentación, por lo que bajo el método se muestra la información registrada tanto del método en si mismo, como de cada uno de los parámetros

La diferencia es abismal, ¿no? En el primer ejemplo, que está sin documentar, somos como un mamut con pistolas pegando tiros a ver qué sale. En cambio, en el segundo caso, tenemos una información que escribió el propio desarrollador del método, donde nos explica qué hace, qué hacen sus parámetros y qué valores de retorno o excepciones pueden existir (si las hubiese).

Para conseguir esta maravilla, simplemente es necesario que en el propio código de la clase introduzcamos unas anotaciones especiales llamadas sumarios, donde vamos a añadir esos mensajes que queremos que se vean desde fuera, por ejemplo:

/// <summary>
/// This is the awesome class that contains the awesome method
/// </summary>
public class MyAwesomeAPI
{
    /// <summary>
    /// This method is awesome and solves your problems
    /// </summary>
    /// <param name="parameter1">This is the first parameter for the awesome method</param>
    /// <param name="parameter2">This is the second parameter for the awesome method</param>
    public void MyAwesomeMethod(int parameter1, int parameter2)
    {
    }
}

Esto es sencillo al estar perfectamente integrado, incluirse en el código y formar parte del propio binario, pero... ¿podemos seguir este planteamiento en una API REST hecha en C#?

OpenAPI al rescate

Pues evidentemente, si estas leyendo esto es porque hay una solución y esa solución se llama OpenAPI. Pero, ¿qué es OpenAPI? Si nos vamos a la definición de su propia web (traducida por mí):

La Especificación OpenAPI (OAS) define una descripción de interfaz estándar y de lenguaje de programación para las API de REST, que permite tanto a los humanos como a las computadoras descubrir y comprender las capacidades de un servicio sin necesidad de acceder al código fuente, a documentación adicional o a la inspección del tráfico de la red. Cuando se define adecuadamente a través de OpenAPI, un consumidor puede comprender e interactuar con el servicio remoto con una cantidad mínima de lógica de implementación. De manera similar a lo que las descripciones de interfaz han hecho para la programación de nivel inferior, la especificación OpenAPI elimina las conjeturas al llamar a un servicio. -- OpenAPI Specification

Y esto, traducido a palabras mortales, quiere decir que la propia API REST va a exponer de manera adicional uno o más endpoints que se pueden consultar para descubrir qué hay en esa API. Lo mínimo que va a ofrecer, es un fichero json en el que se define el funcionamiento de la API REST (endpoints disponibles, verbos, autenticaciones...). El formato de este json está estandarizado y eso permite a aplicaciones y personas entender qué expone una API REST sin tener que hacer "mágica chamánica".

Un ejemplo del json que genera el archiconocido WeatherForecast (la API REST de ejemplo de ASP.NET):

{
  "openapi": "3.0.1",
  "info": {
    "title": "PostSwagger",
    "version": "v1"
  },
  "paths": {
    "/WeatherForecast": {
      "get": {
        "tags": [
          "WeatherForecast"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeatherForecast"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeatherForecast"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeatherForecast"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "WeatherForecast": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "temperatureC": {
            "type": "integer",
            "format": "int32"
          },
          "temperatureF": {
            "type": "integer",
            "format": "int32",
            "readOnly": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      }
    }
  }
}

Aunque sin conocer el formato cuesta un poco saber qué está exponiendo la API REST que nos ofrece el json, más o menos se deja entrever que hay un endpoint en /WeatherForecast, cuyo único retorno definido es un HTTP 200 con un array de tipo WeatherForecast. Este array contiene 4 propiedades llamadas datetemperatureCtemperatureF y summary.

Aunque con devolver este json sería suficiente, a las personas nos suelen gustar más las tablas, los botones, las etiquetas... O sea, todo lo visual y que nos dé menos trabajo... Así que, lo bueno de que el formato sea estándar, es que existen herramientas que nos ofrecen una interfaz gráfica sobre este json, de modo que interactuar se vuelve mucho más sencillo.

"La imagen muestra la interfaz gráfica de Swagger, donde se listan los diferentes endpoints de la API REST categorizados por controladores"

Incluso podemos probar desde el navegador los diferentes endpoint que ofrece:

"La imagen muestra la interfaz gráfica de swagger, señalan los botones 'Try it out', 'Execute' y el resultado de la ejecución dentro del endpoint /WeatherForecast"

Swagger en .Net 5

¿Y cómo podemos tener esta maravilla en nuestras APIs REST?

Si el proyecto que estamos creando es ASP .NET Core en .Net 5, basta con que marquemos el selector Habilitar soporte para OpenAPI:

"La imagen muestra el selector en la interfaz de Visual Studio"

Al hacerlo, se crea automáticamente un proyecto con todo lo necesario para que tanto el fichero swagger.json, como el endpoint de la interfaz gráfica (/swagger/index.html) estén disponibles.

¡Estupendo!

Swagger en .Net Core

¿Y qué podemos hacer si el proyecto no es .Net 5?

Pues, las versiones anteriores no tienen soporte durante la creación del proyecto, pero añadirlo es algo muy sencillo.

Lo primero que vamos a necesitar, es añadir a nuestro proyecto el paquete NuGet Swashbuckle.AspNetCore.

Después, en el método ConfigureServices de la clase StartUp el código, tenemos que añadir esto:

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "NombreDeMiApi", Version = "v1" });
});

Y por último, vamos a registrar el middleware que se va a encargar de servir la interfaz de usuario y el fichero json añadiendo en el método Configure:

app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
                 "NombreDeMiApi v1"));

Por tener una visión completa:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "NombreDeMiApi", Version = "v1" });
    });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseSwagger();
    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
                     "NombreDeMiApi v1"));
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

¡Listo! Con esto conseguimos lo mismo que .Net 5 nos proporciona de serie.

Conclusión

Con solo hacer esto, ya hemos conseguido una configuración básica de nuestra documentación OpenAPI, así como una interfaz de usuario para poder probar. Gracias a ello, automáticamente se detectarán todos los controladores y sus acciones, y se añadirán de manera transparente el fichero json y, por tanto, a la interfaz gráfica que lo utiliza.

Este es el comportamiento por defecto y la documentación básica. Entre nuestro deberes como developers, está el complementar esta documentación para que sea completa, añadiendo cosas como códigos HTTP esperados, autenticaciones si las hay, no mostrar todos los endpoints que no sean necesarios, etc..

Pese a todo, es una herramienta la mar de interesante y que personalmente pienso (y no solo yo por lo que se ve en el cambio de .Net 5) que debería formar parte, por defecto, de todas nuestras APIs REST. Va a permitir que terceros se integren con nosotros, pero incluso para nosotros mismos durante las pruebas nos va a simplificar mucho el trabajo al no tener que estar usando Postman, curl, o cualquier otra herramienta similar.

Por otro lado, y esto es también algo muy interesante, OpenAPI está soportado por Azure API Management (y seguro que por otros proveedores cloud), por lo que si nuestra API REST genera el fichero json, podemos utilizarlo para etapas posteriores del ciclo de vida de nuestro servicio.

Fecha de publicación:
Jorge Turrado Jorge lleva en el mundo de la programación desde los tiempos de .Net Framework 3.0. Es experto en la plataforma .NET, en Kubernetes y en técnicas de integración continua entre otras cosas. Actualmente trabaja como Staff SRE en la empresa SCRM Lidl International Hub. Microsoft lo ha reconocido como MVP en tecnologías de desarrollo, es CNCF Ambassador y maintainer oficial de KEDA, el autoescalador de Kubernetes basado en eventos. Puedes seguirlo en Twitter: @JorgeTurrado o en su blog FixedBuffer Ver todos los posts de Jorge Turrado
Archivado en: Desarrollo Web

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Comentarios (4) -

Carlos Azabache
Carlos Azabache

Buenas noches, ¿Alquién a tenido este problema? por mas vueltas que le doy no hallo la solución ==> No se pudo generar el archivo swagger. Error dotnet swagger tofile --serializeasv2 --output

Esto se da cuando voy a publicar en AZURE, en compilación la API funciona perfecto

De antemano agradezco si alguien me puede ayudar con la solución.

Responder

Jorge Turrado
Jorge Turrado

Buenas Carlos,
Que cosa más rara... no lo he visto nunca :/
¿Podrías contarnos algo más sobre tu escenario a ver si podemos ayudarte?
Lo único que encuentro al respecto es una issue en Github donde hablan de que la ruta en el disco local parece tener algo que ver... github.com/.../1883#issuecomment-726671921
¿Tal vez cuando vas a publicar la ruta se hace muy larga y por eso falla? Tal vez puedas probar a crear una carpeta en C:/ y poner el proyecto ahí directamente para descartar que pueda ser problemas con la longitud de la ruta.

Un saludo

Responder

Carlos Azabache
Carlos Azabache

Hola Jorge, si fue algo raro te cuento que intente con todas las soluciones habidas y por haber de la red y ni una dio la solución.

Lo que hice fue, el proyecto tal y cual lo pase a NET 5 y funcionó perfectamente.

Saludos.

Responder

Daniel Alberto
Daniel Alberto

Tengo un server WebApi con 2 clases y un controlador que hice para probar SWAGGER:
class Marca Campo1

class Camion Campo1 Campo2 Campo3 List Marcas

Controlador CAMIONES [HttpGet] public Get(Camion varCamion)

Desde la interface de SwaggerUI, en el GET del controlador CAMIONES puedo cargar Campo1,2y3 y puedo cargar el array de Marcas.

El Controlador recibe en el Get el objeto Camion en varCamion, pero el Count de Marcas en 0, es decir NO impactan las marcas cargadas en array.

Tengo la imagen de la ventana donde se ve como el controlador recibe el objeto Camion pero la LISTA VACIA (Count=0)
Uso NetCore 2.2, c# y Swashbuckle.AspNetCore 6.1.4
Agradezco mucho si me pueden ayudar
saludos
Daniel

curl -X GET "https://daniels:500/api/CAMIONES?Campo1=valor1111&Campo2=2222&Campo3=3333&Marcas=%7B%0A%20%20%22campo1%22%3A%20%22Toyota%22%0A%7D" -H "accept: text/plain" -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM4OWRmM2M2ZWFhN2UwZWViOTNlNjk3YTFmODgyYjk5IiwidHlwIjoiSldUIn0.eyJuYmYiOjE2MjI3NDIxODEsImV4cCI6MTYyNTMzNDE4MSwiaXNzIjoiaHR0cHM6Ly9kYW5pZWxzOjUwMCIsImF1ZCI6WyJodHRwczovL2RhbmllbHM6NTAwL3Jlc291cmNlcyIsImFwaTEiXSwiY2xpZW50X2lkIjoiQkFTQ1MiLCJzdWIiOiIyMTJjNzhlNy04YmQ3LTQ4MGYtOWFjZi1iNTNkZTUzNGQyMzEiLCJhdXRoX3RpbWUiOjE2MjI3NDIxODAsImlkcCI6ImxvY2FsIiwiTm9tYnJlIjoiZHNhbHVtIiwiRGlzcGxheU5hbWUiOiJEYW5pZWwgU2FsdW0iLCJHdWlkIjoiMjEyYzc4ZTctOGJkNy00ODBmLTlhY2YtYjUzZGU1MzRkMjMxIiwic2NvcGUiOlsib3BlbmlkIiwicGVyZmlsIiwiYXBpMSJdLCJhbXIiOlsicHdkIl19.s7T1Dz3VOYhQxGEc2gmqJreBKgjpJdtqHuSe8xP4eLZgba8eMTqd2fFPoW5xtiXHi116YbhQ448DhosrZHNDX4G4zjE9OqQhNPIyZE4pSNen4SZ7ndLIf2MK7hbxlJMJ0Nlt5ySWE00-hQFSmVXpNe68eaKmVFltjl2HvNS1ptxnHY726l-ZKIDhTQIZfNWSus4DucX5m0sxTLMwGVMy5drw91kulDV9AWb26EFfJyRD7ASEcJu7LbtUChgNf2cAp5zpY4fUjx6GV5a836j1UfxmFYf8ymVldiV2vO1VBw0I8bTRXqRS1bmfV7X06NovyLNqOZwdVQD2iOTwVLasJA"

RequestURL
https://daniels:500/api/CAMIONES?Campo1=valor1111&Campo2=2222&Campo3=3333&Marcas=%7B%0A%20%20%22campo1%22%3A%20%22Toyota%22%0A%7D

Responder

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.