Delen via


Quickstart: Een nieuw API-project maken met TypeSpec en .NET

In deze quickstart leert u hoe u TypeSpec gebruikt om een RESTful API-toepassing te ontwerpen, genereren en implementeren. TypeSpec is een opensource-taal voor het beschrijven van cloudservice-API's en genereert client- en servercode voor meerdere platforms. Door deze quickstart te volgen, leert u hoe u uw API-contract eenmaal definieert en consistente implementaties genereert, zodat u beter onderhoudbare en goed gedocumenteerde API-services kunt bouwen.

U gebruikt de codegenerator van TypeSpec om een ASP.NET Core-server te maken met routering en Swagger UI, deze te verbinden met Azure Cosmos DB voor persistentie en te implementeren in Azure Container Apps.

Zie Overzicht van TypeSpec voor context over de rol van TypeSpec in API-ontwikkeling.

Tijd om te voltooien: 20-25 minuten

In deze snelstart, gaat u het volgende doen:

  • Uw API definiëren met Behulp van TypeSpec
  • Een API-servertoepassing maken
  • Azure Cosmos DB integreren voor permanente opslag
  • Uw API lokaal uitvoeren en testen
  • Implementeren in Azure Container Apps

Prerequisites

Ontwikkelen met TypeSpec

TypeSpec definieert uw API op een taalagnostische manier en genereert de API-server en clientbibliotheek voor meerdere platforms. Met deze functionaliteit kunt u het volgende doen:

  • Uw API-contract eenmaal definiëren
  • Consistente server- en clientcode genereren
  • Focus op het implementeren van bedrijfslogica in plaats van API-infrastructuur

TypeSpec biedt API-servicebeheer:

  • API-definitietaal
  • Middleware voor routering aan de serverzijde voor API
  • Clientbibliotheken voor het verbruik van API

U verstrekt clientaanvragen en serverintegraties:

  • Bedrijfslogica implementeren in middleware zoals Azure-services voor databases, opslag en berichten
  • Hostingserver voor uw API (lokaal of in Azure)
  • Implementatiescripts voor herhaalbare inrichting en implementatie

Een nieuwe TypeSpec-toepassing maken

  1. Maak een nieuwe map voor de API-server en TypeSpec-bestanden.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Installeer de TypeSpec-compiler globaal:

    npm install -g @typespec/compiler
    
  3. Controleer of TypeSpec juist is geïnstalleerd:

    tsp --version
    
  4. Initialiseer het TypeSpec-project:

    tsp init
    
  5. Beantwoord de volgende prompts met de opgegeven antwoorden:

    • Initialiseer hier een nieuw project? Y
    • Selecteer een projectsjabloon? Algemene REST API
    • Voer een projectnaam in: Widgets
    • Welke emitters wilt u gebruiken?
      • OpenAPI 3.1-document
      • C# serverstubs

    TypeSpec-emitters zijn bibliotheken die gebruikmaken van verschillende TypeSpec-compiler-API's om te weerspiegelen over het TypeSpec-compilatieproces en artefacten te genereren.

  6. Wacht totdat de initialisatie is voltooid voordat u doorgaat.

    Run tsp compile . to build the project.
    
    Please review the following messages from emitters:
      @typespec/http-server-csharp: 
    
            Generated ASP.Net services require dotnet 9:
            https://dotnet.microsoft.com/download 
    
            Create an ASP.Net service project for your TypeSpec:
            > npx hscs-scaffold . --use-swaggerui --overwrite
    
            More information on getting started:
            https://aka.ms/tsp/hscs/start
    
  7. Compileer het project:

    tsp compile .
    

    Aanbeveling

    Voor iteratieve ontwikkeling gebruikt u de controlemodus om automatisch opnieuw te compileren bij bestandswijzigingen: tsp compile . --watch Hierdoor blijven de gegenereerde server en het schema up-to-date terwijl u deze wijzigt main.tsp.

  8. TypeSpec genereert het standaardproject in ./tsp-output, waarbij twee afzonderlijke mappen worden gemaakt:

    • schema
    • server
  9. Open het ./tsp-output/schema/openapi.yaml-bestand. U ziet dat de weinige regels in ./main.tsp meer dan 200 regels OpenAPI-specificatie hebben gegenereerd.

  10. Open de map ./tsp-output/server/aspnet. U ziet dat de gescaffolde .NET-bestanden het volgende bevatten:

    • ./generated/operations/IWidgets.cs definieert de interface voor de widgetsmethoden.
    • ./generated/controllers/WidgetsController.cs implementeert de integratie met de widgetsmethoden. Hier plaatst u uw bedrijfslogica.
    • ./generated/models definieert de modellen voor de Widget-API.

TypeSpec emitters configureren

Gebruik de TypeSpec-bestanden om het genereren van de API-server te configureren.

  1. Open de tspconfig.yaml bestaande configuratie en vervang deze door de volgende YAML:

    emit:
      - "@typespec/openapi3"
      - "@typespec/http-server-csharp"
    options:
      "@typespec/openapi3":
        emitter-output-dir: "{cwd}/server/wwwroot"
        openapi-versions:
          - 3.1.0
      "@typespec/http-server-csharp":
        emitter-output-dir: "{cwd}/server/"
        use-swaggerui: true
        overwrite: true
        emit-mocks: "mocks-and-project-files"
    

    Deze configuratie projecteert verschillende wijzigingen die we nodig hebben voor een volledig gegenereerde .NET API-server:

    • emit-mocks: Maak alle projectbestanden die nodig zijn voor de server.
    • use-swaggerui: Integreer de Swagger-gebruikersinterface zodat u de API op een browservriendelijke manier kunt gebruiken.
    • emitter-output-dir: Stel de uitvoermap in voor zowel het genereren van servers als het genereren van OpenApi-specificatie.
    • Genereer alles in ./server.
  2. Het project opnieuw compileren:

    tsp compile .
    
  3. Ga naar de nieuwe /server directory:

    cd server
    
  4. Maak een standaardcertificaat voor ontwikkelaars als u er nog geen hebt:

    dotnet dev-certs https
    
  5. Voer het project uit:

    dotnet run
    

    Wacht totdat de melding is geopend in de browser.

  6. Open de browser en voeg de Swagger UI-route toe. /swagger

    Een schermopname van de Swagger-gebruikersinterface met de Widgets-API.

  7. De standaard TypeSpec-API en de server werken allebei.

Inzicht in de structuur van toepassingsbestanden

De projectstructuur voor de gegenereerde server bevat de API-server op basis van de .NET-controller, de .NET-bestanden voor het bouwen van het project en de middleware voor uw Azure-integratie.

├── appsettings.Development.json
├── appsettings.json
├── docs
├── generated
├── mocks
├── Program.cs
├── Properties
├── README.md
├── ServiceProject.csproj
└── wwwroot
  • Voeg uw bedrijfslogica toe: begin in dit voorbeeld met het ./server/mocks/Widget.cs bestand. De gegenereerde Widget.cs biedt standaardmethoden.
  • Werk de server bij: voeg eventuele specifieke serverconfiguraties toe aan ./program.cs.
  • Gebruik de OpenApi-specificatie: de TypeSpec heeft het OpenApi3.json bestand gegenereerd in het ./server/wwwroot bestand geplaatst en het beschikbaar gemaakt voor Swagger UI tijdens de ontwikkeling. Dit biedt een gebruikersinterface voor uw specificatie. U kunt communiceren met uw API zonder dat u een aanvraagmechanisme hoeft op te geven, zoals een REST-client of webfront-end.

Persistentie wijzigen in Azure Cosmos DB no-sql

Nu de basiswidget-API-server werkt, werkt u de server bij om te werken met Azure Cosmos DB voor een permanent gegevensarchief.

  1. Voeg ./server toe aan het project in de map:

    dotnet add package Microsoft.Azure.Cosmos
    
  2. Voeg de Azure Identity-bibliotheek toe om te verifiëren bij Azure:

    dotnet add package Azure.Identity
    
  3. Werk de ./server/ServiceProject.csproj integratie-instellingen voor Cosmos DB bij:

    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        ... existing settings ...
        <EnableSdkContainerSupport>true</EnableSdkContainerSupport>
      </PropertyGroup>
      <ItemGroup>
        ... existing settings ...
        <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
      </ItemGroup>
    </Project>
    
    • Met EnableSdkContainerSupport kunt u de ingebouwde ondersteuning voor containerbuilding van de .NET SDK (dotnet publish ––container) gebruiken zonder een Dockerfile te schrijven.
    • Newtonsoft.Json voegt de Json .NET-serializer toe die door de Cosmos DB SDK wordt gebruikt om uw .NET-objecten te converteren naar en van JSON.
  4. Maak een nieuw registratiebestand ./azure/CosmosDbRegistration om de Cosmos DB-registratie te beheren:

    using Microsoft.Azure.Cosmos;
    using Microsoft.Extensions.Configuration;
    using System;
    using System.Threading.Tasks;
    using Azure.Identity;
    using DemoService;
    
    namespace WidgetService.Service
    {
        /// <summary>
        /// Registration class for Azure Cosmos DB services and implementations
        /// </summary>
        public static class CosmosDbRegistration
        {
            /// <summary>
            /// Registers the Cosmos DB client and related services for dependency injection
            /// </summary>
            /// <param name="builder">The web application builder</param>
            public static void RegisterCosmosServices(this WebApplicationBuilder builder)
            {
                // Register the HttpContextAccessor for accessing the HTTP context
                builder.Services.AddHttpContextAccessor();
    
                // Get configuration settings
                var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"];
    
                // Validate configuration
                ValidateCosmosDbConfiguration(cosmosEndpoint);
    
                // Configure Cosmos DB client options
                var cosmosClientOptions = new CosmosClientOptions
                {
                    SerializerOptions = new CosmosSerializationOptions
                    {
                        PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
                    },
                    ConnectionMode = ConnectionMode.Direct
                };
    
                builder.Services.AddSingleton(serviceProvider =>
                {
                    var credential = new DefaultAzureCredential();
    
                    // Create Cosmos client with token credential authentication
                    return new CosmosClient(cosmosEndpoint, credential, cosmosClientOptions);
                });
    
                // Initialize Cosmos DB if needed
                builder.Services.AddHostedService<CosmosDbInitializer>();
    
                // Register WidgetsCosmos implementation of IWidgets
                builder.Services.AddScoped<IWidgets, WidgetsCosmos>();
            }
    
            /// <summary>
            /// Validates the Cosmos DB configuration settings
            /// </summary>
            /// <param name="cosmosEndpoint">The Cosmos DB endpoint</param>
            /// <exception cref="ArgumentException">Thrown when configuration is invalid</exception>
            private static void ValidateCosmosDbConfiguration(string cosmosEndpoint)
            {
                if (string.IsNullOrEmpty(cosmosEndpoint))
                {
                    throw new ArgumentException("Cosmos DB Endpoint must be specified in configuration");
                }
            }
        }
    }
    

    Let op de omgevingsvariabele voor het eindpunt:

    var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"];
    
  5. Maak een nieuwe widgetklasse om ./azure/WidgetsCosmos.cs bedrijfslogica te bieden die kan worden geïntegreerd met Azure Cosmos DB voor uw permanente opslag.

    using System;
    using System.Net;
    using System.Threading.Tasks;
    using Microsoft.Azure.Cosmos;
    using Microsoft.Extensions.Logging;
    using System.Collections.Generic;
    using System.Linq;
    
    // Use generated models and operations
    using DemoService;
    
    namespace WidgetService.Service
    {
        /// <summary>
        /// Implementation of the IWidgets interface that uses Azure Cosmos DB for persistence
        /// </summary>
        public class WidgetsCosmos : IWidgets
        {
            private readonly CosmosClient _cosmosClient;
            private readonly ILogger<WidgetsCosmos> _logger;
            private readonly IHttpContextAccessor _httpContextAccessor;
            private readonly string _databaseName = "WidgetDb";
            private readonly string _containerName = "Widgets";
    
            /// <summary>
            /// Initializes a new instance of the WidgetsCosmos class.
            /// </summary>
            /// <param name="cosmosClient">The Cosmos DB client instance</param>
            /// <param name="logger">Logger for diagnostic information</param>
            /// <param name="httpContextAccessor">Accessor for the HTTP context</param>
            public WidgetsCosmos(
                CosmosClient cosmosClient,
                ILogger<WidgetsCosmos> logger,
                IHttpContextAccessor httpContextAccessor)
            {
                _cosmosClient = cosmosClient;
                _logger = logger;
                _httpContextAccessor = httpContextAccessor;
            }
    
            /// <summary>
            /// Gets a reference to the Cosmos DB container for widgets
            /// </summary>
            private Container WidgetsContainer => _cosmosClient.GetContainer(_databaseName, _containerName);
    
            /// <summary>
            /// Lists all widgets in the database
            /// </summary>
            /// <returns>Array of Widget objects</returns>
            public async Task<WidgetList> ListAsync()
            {
                try
                {
                    var queryDefinition = new QueryDefinition("SELECT * FROM c");
                    var widgets = new List<Widget>();
    
                    using var iterator = WidgetsContainer.GetItemQueryIterator<Widget>(queryDefinition);
                    while (iterator.HasMoreResults)
                    {
                        var response = await iterator.ReadNextAsync();
                        widgets.AddRange(response.ToList());
                    }
    
                    // Create and return a WidgetList instead of Widget[]
                    return new WidgetList
                    {
                        Items = widgets.ToArray()
                    };
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error listing widgets from Cosmos DB");
                    throw new Error(500, "Failed to retrieve widgets from database");
                }
            }
    
            /// <summary>
            /// Retrieves a specific widget by ID
            /// </summary>
            /// <param name="id">The ID of the widget to retrieve</param>
            /// <returns>The retrieved Widget</returns>
            public async Task<Widget> ReadAsync(string id)
            {
                try
                {
                    var response = await WidgetsContainer.ReadItemAsync<Widget>(
                        id, new PartitionKey(id));
    
                    return response.Resource;
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    _logger.LogWarning("Widget with ID {WidgetId} not found", id);
                    throw new Error(404, $"Widget with ID '{id}' not found");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error reading widget {WidgetId} from Cosmos DB", id);
                    throw new Error(500, "Failed to retrieve widget from database");
                }
            }
            /// <summary>
            /// Creates a new widget from the provided Widget object
            /// </summary>
            /// <param name="body">The Widget object to store in the database</param>
            /// <returns>The created Widget</returns>
            public async Task<Widget> CreateAsync(Widget body)
            {
                try
                {
                    // Validate the Widget
                    if (body == null)
                    {
                        throw new Error(400, "Widget data cannot be null");
                    }
    
                    if (string.IsNullOrEmpty(body.Id))
                    {
                        throw new Error(400, "Widget must have an Id");
                    }
    
                    if (body.Color != "red" && body.Color != "blue")
                    {
                        throw new Error(400, "Color must be 'red' or 'blue'");
                    }
    
                    // Save the widget to Cosmos DB
                    var response = await WidgetsContainer.CreateItemAsync(
                        body, new PartitionKey(body.Id));
    
                    _logger.LogInformation("Created widget with ID {WidgetId}", body.Id);
                    return response.Resource;
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
                {
                    _logger.LogError(ex, "Widget with ID {WidgetId} already exists", body.Id);
                    throw new Error(409, $"Widget with ID '{body.Id}' already exists");
                }
                catch (Exception ex) when (!(ex is Error))
                {
                    _logger.LogError(ex, "Error creating widget in Cosmos DB");
                    throw new Error(500, "Failed to create widget in database");
                }
            }
    
            /// <summary>
            /// Updates an existing widget with properties specified in the patch document
            /// </summary>
            /// <param name="id">The ID of the widget to update</param>
            /// <param name="body">The WidgetMergePatchUpdate object containing properties to update</param>
            /// <returns>The updated Widget</returns>
            public async Task<Widget> UpdateAsync(string id, TypeSpec.Http.WidgetMergePatchUpdate body)
            {
                try
                {
                    // Validate input parameters
                    if (body == null)
                    {
                        throw new Error(400, "Update data cannot be null");
                    }
    
                    if (body.Color != null && body.Color != "red" && body.Color != "blue")
                    {
                        throw new Error(400, "Color must be 'red' or 'blue'");
                    }
    
                    // First check if the item exists
                    Widget existingWidget;
                    try
                    {
                        var response = await WidgetsContainer.ReadItemAsync<Widget>(
                            id, new PartitionKey(id));
                        existingWidget = response.Resource;
                    }
                    catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                    {
                        _logger.LogWarning("Widget with ID {WidgetId} not found for update", id);
                        throw new Error(404, $"Widget with ID '{id}' not found");
                    }
    
                    // Apply the patch updates only where properties are provided
                    bool hasChanges = false;
    
                    if (body.Weight.HasValue)
                    {
                        existingWidget.Weight = body.Weight.Value;
                        hasChanges = true;
                    }
    
                    if (body.Color != null)
                    {
                        existingWidget.Color = body.Color;
                        hasChanges = true;
                    }
    
                    // Only perform the update if changes were made
                    if (hasChanges)
                    {
                        // Use ReplaceItemAsync for the update
                        var updateResponse = await WidgetsContainer.ReplaceItemAsync(
                            existingWidget, id, new PartitionKey(id));
    
                        _logger.LogInformation("Updated widget with ID {WidgetId}", id);
                        return updateResponse.Resource;
                    }
    
                    // If no changes, return the existing widget
                    _logger.LogInformation("No changes to apply for widget with ID {WidgetId}", id);
                    return existingWidget;
                }
                catch (Error)
                {
                    // Rethrow Error exceptions
                    throw;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error updating widget {WidgetId} in Cosmos DB", id);
                    throw new Error(500, "Failed to update widget in database");
                }
            }
    
            /// <summary>
            /// Deletes a widget by its ID
            /// </summary>
            /// <param name="id">The ID of the widget to delete</param>
            public async Task DeleteAsync(string id)
            {
                try
                {
                    await WidgetsContainer.DeleteItemAsync<Widget>(id, new PartitionKey(id));
                    _logger.LogInformation("Deleted widget with ID {WidgetId}", id);
                }
                catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                {
                    _logger.LogWarning("Widget with ID {WidgetId} not found for deletion", id);
                    throw new Error(404, $"Widget with ID '{id}' not found");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error deleting widget {WidgetId} from Cosmos DB", id);
                    throw new Error(500, "Failed to delete widget from database");
                }
            }
    
            /// <summary>
            /// Analyzes a widget by ID and returns a simplified analysis result
            /// </summary>
            /// <param name="id">The ID of the widget to analyze</param>
            /// <returns>An AnalyzeResult containing the analysis of the widget</returns>
            public async Task<AnalyzeResult> AnalyzeAsync(string id)
            {
                try
                {
                    // First retrieve the widget from the database
                    Widget widget;
                    try
                    {
                        var response = await WidgetsContainer.ReadItemAsync<Widget>(
                            id, new PartitionKey(id));
                        widget = response.Resource;
                    }
                    catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
                    {
                        _logger.LogWarning("Widget with ID {WidgetId} not found for analysis", id);
                        throw new Error(404, $"Widget with ID '{id}' not found");
                    }
    
                    // Create the analysis result
                    var result = new AnalyzeResult
                    {
                        Id = widget.Id,
                        Analysis = $"Weight: {widget.Weight}, Color: {widget.Color}"
                    };
    
                    _logger.LogInformation("Analyzed widget with ID {WidgetId}", id);
                    return result;
                }
                catch (Error)
                {
                    // Rethrow Error exceptions
                    throw;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error analyzing widget {WidgetId} from Cosmos DB", id);
                    throw new Error(500, "Failed to analyze widget from database");
                }
            }
        }
    }
    
  6. Maak het ./server/services/CosmosDbInitializer.cs bestand om te verifiëren bij Azure:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.Cosmos;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    
    namespace WidgetService.Service
    {
        /// <summary>
        /// Hosted service that initializes Cosmos DB resources on application startup
        /// </summary>
        public class CosmosDbInitializer : IHostedService
        {
            private readonly CosmosClient _cosmosClient;
            private readonly ILogger<CosmosDbInitializer> _logger;
            private readonly IConfiguration _configuration;
            private readonly string _databaseName;
            private readonly string _containerName = "Widgets";
    
            public CosmosDbInitializer(CosmosClient cosmosClient, ILogger<CosmosDbInitializer> logger, IConfiguration configuration)
            {
                _cosmosClient = cosmosClient;
                _logger = logger;
                _configuration = configuration;
                _databaseName = _configuration["CosmosDb:DatabaseName"] ?? "WidgetDb";
            }
    
            public async Task StartAsync(CancellationToken cancellationToken)
            {
                _logger.LogInformation("Ensuring Cosmos DB database and container exist...");
    
                try
                {
                    // Create database if it doesn't exist
                    var databaseResponse = await _cosmosClient.CreateDatabaseIfNotExistsAsync(
                        _databaseName,
                        cancellationToken: cancellationToken);
    
                    _logger.LogInformation("Database {DatabaseName} status: {Status}", _databaseName,
                        databaseResponse.StatusCode == System.Net.HttpStatusCode.Created ? "Created" : "Already exists");
    
                    // Create container if it doesn't exist (using id as partition key)
                    var containerResponse = await databaseResponse.Database.CreateContainerIfNotExistsAsync(
                        new ContainerProperties
                        {
                            Id = _containerName,
                            PartitionKeyPath = "/id"
                        },
                        throughput: 400, // Minimum RU/s
                        cancellationToken: cancellationToken);
    
                    _logger.LogInformation("Container {ContainerName} status: {Status}", _containerName,
                        containerResponse.StatusCode == System.Net.HttpStatusCode.Created ? "Created" : "Already exists");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error initializing Cosmos DB");
                    throw;
                }
            }
    
            public Task StopAsync(CancellationToken cancellationToken)
            {
                return Task.CompletedTask;
            }
        }
    }
    
  7. Werk de ./server/program.cs bij om Cosmos DB te gebruiken en sta toe dat de Swagger-gebruikersinterface kan worden gebruikt in een productie-uitrol. Kopiëren in het hele bestand:

    // Generated by @typespec/http-server-csharp
    // <auto-generated />
    #nullable enable
    
    using TypeSpec.Helpers;
    using WidgetService.Service;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddControllersWithViews(options =>
    {
        options.Filters.Add<HttpServiceExceptionFilter>();
    });
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    // Replace original registration with the Cosmos DB one
    CosmosDbRegistration.RegisterCosmosServices(builder);
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    // Swagger UI is always available
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.DocumentTitle = "TypeSpec Generated OpenAPI Viewer";
        c.SwaggerEndpoint("/openapi.yaml", "TypeSpec Generated OpenAPI Docs");
        c.RoutePrefix = "swagger";
    });
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.Use(async (context, next) =>
    {
        context.Request.EnableBuffering();
        await next();
    });
    
    app.MapGet("/openapi.yaml", async (HttpContext context) =>
    {
        var externalFilePath = "wwwroot/openapi.yaml"; 
        if (!File.Exists(externalFilePath))
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            await context.Response.WriteAsync("OpenAPI spec not found.");
            return;
        }
        context.Response.ContentType = "application/json";
        await context.Response.SendFileAsync(externalFilePath);
    });
    
    app.UseRouting();
    app.UseAuthorization();
    
    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    
    app.Run();
    
  8. Bouw het project:

    dotnet build
    

    Het project bouwt nu met Cosmos DB-integratie. We gaan de implementatiescripts maken om de Azure-resources te maken en het project te implementeren.

Implementatie-infrastructuur maken

Maak de bestanden die nodig zijn voor een herhaalbare implementatie met Azure Developer CLI en Bicep-sjablonen.

  1. Maak in de hoofdmap van uw TypeSpec-project een azure.yaml implementatiedefinitiebestand en plak de volgende brontekst:

    # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
    
    name: azure-typespec-scaffold-dotnet
    metadata:
        template: azd-init@1.14.0
    services:
        api:
            project: ./server
            host: containerapp
            language: dotnet
    pipeline:
      provider: github
    

    U ziet dat deze configuratie verwijst naar de gegenereerde projectlocatie (./server). Zorg ervoor dat ./tspconfig.yaml deze overeenkomt met de locatie die is opgegeven in ./azure.yaml.

  2. Maak een ./infra map in de hoofdmap van het TypeSpec-project.

  3. Maak een ./infra/main.bicepparam bestand en kopieer het volgende om de parameters te definiëren die we nodig hebben voor implementatie:

    using './main.bicep'
    
    param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'dev')
    param location = readEnvironmentVariable('AZURE_LOCATION', 'eastus2')
    param deploymentUserPrincipalId = readEnvironmentVariable('AZURE_PRINCIPAL_ID', '')
    

    Deze parameterslijst bevat de minimale parameters die nodig zijn voor deze implementatie.

  4. Maak een ./infra/main.bicep bestand en kopieer het volgende om de Azure-resources voor inrichting en implementatie te definiëren:

    metadata description = 'Bicep template for deploying a GitHub App using Azure Container Apps and Azure Container Registry.'
    
    targetScope = 'resourceGroup'
    param serviceName string = 'api'
    var databaseName = 'WidgetDb'
    var containerName = 'Widgets'
    
    @minLength(1)
    @maxLength(64)
    @description('Name of the environment that can be used as part of naming resource convention')
    param environmentName string
    
    @minLength(1)
    @description('Primary location for all resources')
    param location string
    
    @description('Id of the principal to assign database and application roles.')
    param deploymentUserPrincipalId string = ''
    
    var resourceToken = toLower(uniqueString(resourceGroup().id, environmentName, location))
    
    var tags = {
      'azd-env-name': environmentName
      repo: 'https://github.com/typespec'
    }
    
    module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
      name: 'user-assigned-identity'
      params: {
        name: 'identity-${resourceToken}'
        location: location
        tags: tags
      }
    }
    
    module cosmosDb 'br/public:avm/res/document-db/database-account:0.8.1' = {
      name: 'cosmos-db-account'
      params: {
        name: 'cosmos-db-nosql-${resourceToken}'
        location: location
        locations: [
          {
            failoverPriority: 0
            locationName: location
            isZoneRedundant: false
          }
        ]
        tags: tags
        disableKeyBasedMetadataWriteAccess: true
        disableLocalAuth: true
        networkRestrictions: {
          publicNetworkAccess: 'Enabled'
          ipRules: []
          virtualNetworkRules: []
        }
        capabilitiesToAdd: [
          'EnableServerless'
        ]
        sqlRoleDefinitions: [
          {
            name: 'nosql-data-plane-contributor'
            dataAction: [
              'Microsoft.DocumentDB/databaseAccounts/readMetadata'
              'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
              'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'
            ]
          }
        ]
        sqlRoleAssignmentsPrincipalIds: union(
          [
            managedIdentity.outputs.principalId
          ],
          !empty(deploymentUserPrincipalId) ? [deploymentUserPrincipalId] : []
        )
        sqlDatabases: [
          {
            name: databaseName
            containers: [
              {
                name: containerName
                paths: [
                  '/id'
                ]
              }
            ]
          }
        ]
      }
    }
    
    module containerRegistry 'br/public:avm/res/container-registry/registry:0.5.1' = {
      name: 'container-registry'
      params: {
        name: 'containerreg${resourceToken}'
        location: location
        tags: tags
        acrAdminUserEnabled: false
        anonymousPullEnabled: true
        publicNetworkAccess: 'Enabled'
        acrSku: 'Standard'
      }
    }
    
    var containerRegistryRole = subscriptionResourceId(
      'Microsoft.Authorization/roleDefinitions',
      '8311e382-0749-4cb8-b61a-304f252e45ec'
    ) 
    
    module registryUserAssignment 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.1' = if (!empty(deploymentUserPrincipalId)) {
      name: 'container-registry-role-assignment-push-user'
      params: {
        principalId: deploymentUserPrincipalId
        resourceId: containerRegistry.outputs.resourceId
        roleDefinitionId: containerRegistryRole
      }
    }
    
    module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = {
      name: 'log-analytics-workspace'
      params: {
        name: 'log-analytics-${resourceToken}'
        location: location
        tags: tags
      }
    }
    
    module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.8.0' = {
      name: 'container-apps-env'
      params: {
        name: 'container-env-${resourceToken}'
        location: location
        tags: tags
        logAnalyticsWorkspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
        zoneRedundant: false
      }
    }
    
    module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = {
      name: 'container-apps-app'
      params: {
        name: 'container-app-${resourceToken}'
        environmentResourceId: containerAppsEnvironment.outputs.resourceId
        location: location
        tags: union(tags, { 'azd-service-name': serviceName })
        ingressTargetPort: 8080
        ingressExternal: true
        ingressTransport: 'auto'
        stickySessionsAffinity: 'sticky'
        scaleMaxReplicas: 1
        scaleMinReplicas: 1
        corsPolicy: {
          allowCredentials: true
          allowedOrigins: [
            '*'
          ]
        }
        managedIdentities: {
          systemAssigned: false
          userAssignedResourceIds: [
            managedIdentity.outputs.resourceId
          ]
        }
        secrets: {
          secureList: [
            {
              name: 'azure-cosmos-db-nosql-endpoint'
              value: cosmosDb.outputs.endpoint
            }
            {
              name: 'user-assigned-managed-identity-client-id'
              value: managedIdentity.outputs.clientId
            }
          ]
        }
        containers: [
          {
            image: 'mcr.microsoft.com/dotnet/samples:aspnetapp-9.0'
            name: serviceName
            resources: {
              cpu: '0.25'
              memory: '.5Gi'
            }
            env: [
              {
                name: 'CONFIGURATION__AZURECOSMOSDB__ENDPOINT'
                secretRef: 'azure-cosmos-db-nosql-endpoint'
              }
              {
                name: 'AZURE_CLIENT_ID'
                secretRef: 'user-assigned-managed-identity-client-id'
              }
            ]
          }
        ]
      }
    }
    
    output CONFIGURATION__AZURECOSMOSDB__ENDPOINT string = cosmosDb.outputs.endpoint
    output CONFIGURATION__AZURECOSMOSDB__DATABASENAME string = databaseName
    output CONFIGURATION__AZURECOSMOSDB__CONTAINERNAME string = containerName
    
    output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
    

    Met de uitvoervariabelen kunt u de ingerichte cloudresources gebruiken met uw lokale ontwikkeling.

  5. De containerAppsApp-tag maakt gebruik van de serviceName-variabele (ingesteld op api boven aan het bestand) en de api opgegeven in ./azure.yaml. Deze verbinding vertelt de Azure Developer CLI waar het .NET-project moet worden geïmplementeerd in de Azure Container Apps-hostingresource.

    ...bicep...
    
    module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = {
      name: 'container-apps-app'
      params: {
        name: 'container-app-${resourceToken}'
        environmentResourceId: containerAppsEnvironment.outputs.resourceId
        location: location
        tags: union(tags, { 'azd-service-name': serviceName })                    <--------- `API`
    
    ...bicep..
    

Projectstructuur

De uiteindelijke projectstructuur bevat de TypeSpec API-bestanden, de Express.js-server en de Azure-implementatiebestanden:

├── infra
├── tsp-output
├── .gitignore
├── .azure.yaml
├── Dockerfile
├── main.tsp
├── package-lock.json
├── package.json
├── tspconfig.yaml
Area Bestanden/mappen
TypeSpec main.tsp, tspconfig.yaml
Express.js server ./tsp-output/server/ (bevat gegenereerde bestanden zoals controllers/, models/, ServiceProject.csproj)
Azure Developer CLI-implementatie ./azure.yaml,./infra/

Toepassing implementeren in Azure

U kunt deze toepassing implementeren in Azure met behulp van Azure Container Apps:

  1. Verifiëren bij de Azure Developer CLI:

    azd auth login
    
  2. Implementeren in Azure Container Apps met behulp van de Azure Developer CLI:

    azd up
    

Toepassing gebruiken in browser

Zodra de implementatie is uitgevoerd, kunt u het volgende doen:

  1. Open de Swagger-gebruikersinterface om uw API te testen op /swagger.
  2. Gebruik de functie Nu uitproberen in elke API om widgets te maken, lezen, bijwerken en verwijderen via de API.

Uw toepassing vergroten

Nu het volledige end-to-endproces werkt, gaat u verder met het bouwen van uw API:

  • Meer informatie over de TypeSpec-taal om meer API's en API-laagfuncties toe te voegen in de ./main.tsp.
  • Voeg aanvullende emitters toe en configureer hun parameters in de ./tspconfig.yaml.
  • Naarmate u meer functies aan uw TypeSpec-bestanden toevoegt, kunt u deze wijzigingen ondersteunen met broncode in het serverproject.
  • Ga door met het gebruik van verificatie zonder wachtwoord met Azure Identity.

De hulpbronnen opschonen

Wanneer u klaar bent met deze quickstart, kunt u de Azure-resources verwijderen:

azd down

Of verwijder de resourcegroep rechtstreeks vanuit Azure Portal.

Troubleshooting

Niet voldaan aan .NET 9-vereiste

Fout:Build error: This project requires .NET 9. You have <version> installed.

Solution:

  1. Controleer de .NET-versie: dotnet --version.
  2. Installeer .NET 9 vanuit dot.net.

HTTPS-certificaatfout

Fout:System.IO.IOException: The certificate generation failed bij het uitvoeren van dotnet dev-certs https

Solution:

  1. Vertrouw het bestaande certificaat: dotnet dev-certs https --trust.
  2. Als dat mislukt, reinigt en maakt u het opnieuw aan: dotnet dev-certs https --clean, en dan dotnet dev-certs https.
  3. Zorg ervoor dat u de terminal als beheerder uitvoert in Windows.

@typespec/http-server-csharp emitterconflict

Fout:Emitter error: Dependency conflict bij de uitvoering tsp compile .

Solution:

  1. Controleer of tspconfig.yaml zowel emit: als (beide) emitters bevat.

    emit:
      - "@typespec/openapi3"
      - "@typespec/http-server-csharp"
    
  2. Cache wissen: npm cache clean --forceen probeer het vervolgens opnieuw: tsp compile ..

Cosmos DB-verificatie mislukt

Fout:Azure.Identity.AuthenticationFailedException Of Connection string/key not set

Solution:

  1. Zorg ervoor dat de omgevingsvariabele is ingesteld: echo $AZURE_COSMOS_ENDPOINT (macOS/Linux) of echo %AZURE_COSMOS_ENDPOINT% (Windows).
  2. Voor lokale ontwikkeling stelt u de waarde in Program.cs of een .env bestand in.
  3. Voor productie moet u ervoor zorgen dat het container-app-geheim is geconfigureerd (zie de sectie Implementatie-infrastructuur maken).

Swagger UI toont een 404-fout

Fout: De browser toont 404 Not Found bij /swagger

Solution:

  1. Controleer de configuratie van het Swagger-eindpunt in Program.cs:

    c.SwaggerEndpoint("/openapi.yaml", "TypeSpec Generated OpenAPI Docs");
    c.RoutePrefix = "swagger";
    
  2. Zorg ervoor dat openapi.yaml deze zich in de wwwroot/ map bevindt.

  3. Herbouwen en opnieuw opstarten: dotnet clean && dotnet build && dotnet run.

Volgende stappen