Rövid útmutató: Új API-projekt létrehozása TypeSpec és .NET használatával

Ebben a rövid útmutatóban megtudhatja, hogyan használhatja a TypeSpec-et RESTful API-alkalmazások tervezésére, létrehozására és implementálására. A TypeSpec egy nyílt forráskódú nyelv a felhőszolgáltatás API-k leírásához, és több platform ügyfél- és kiszolgálókódját hozza létre. A rövid útmutatót követve megtanulhatja, hogyan határozhatja meg egyszer az API-szerződést, és hogyan hozhat létre konzisztens implementációkat, segítve a karbantartható és jól dokumentált API-szolgáltatások kiépítését.

A TypeSpec kódgenerátorával egy ASP.NET Core-kiszolgálót hozhat létre útválasztással és Swagger felhasználói felülettel, csatlakoztathatja azt az Azure Cosmos DB-hez a megőrzése érdekében, és üzembe helyezheti az Azure Container Appsben.

A TypeSpec API-fejlesztésben betöltött szerepének kontextusáról lásd a TypeSpec áttekintését.

Befejezési idő: 20–25 perc

Ebben a gyors kezdő útmutatóban a következőket teheti meg:

  • Az API definiálása a TypeSpec használatával
  • API-kiszolgálóalkalmazás létrehozása
  • Az Azure Cosmos DB integrálása állandó tároláshoz
  • Az API helyi futtatása és tesztelése
  • Üzembe helyezés az Azure Container Appsben

Prerequisites

Fejlesztés a TypeSpec használatával

A TypeSpec nyelvfüggetlen módon határozza meg az API-t, és létrehozza az API-kiszolgálót és az ügyfélkódtárat több platformhoz. Ez a funkció a következő funkciókat teszi lehetővé:

  • API-szerződésed határozd meg egyszer
  • Konzisztens kiszolgáló- és ügyfélkód létrehozása
  • Az API-infrastruktúra helyett az üzleti logika implementálására összpontosítson

A TypeSpec API-szolgáltatáskezelést biztosít:

  • API-definíció nyelve
  • Kiszolgálóoldali útválasztási köztes szoftver API-hoz
  • Ügyfélkódtárak az API használatához

Ügyfélkéréseket és kiszolgálóintegrációkat biztosít:

  • Üzleti logika implementálása köztes szoftverben, például azure-szolgáltatások adatbázisokhoz, tároláshoz és üzenetkezeléshez
  • Kiszolgáló üzemeltetése az API-hoz (helyileg vagy az Azure-ban)
  • Üzembe helyezési szkriptek megismételhető erőforrás-előkészítéshez és üzembe helyezéshez

Új TypeSpec-alkalmazás létrehozása

  1. Hozzon létre egy új mappát az API-kiszolgáló és a TypeSpec fájlok tárolásához.

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. Telepítse a TypeSpec fordítót globálisan:

    npm install -g @typespec/compiler
    
  3. Ellenőrizze, hogy a TypeSpec megfelelően van-e telepítve:

    tsp --version
    
  4. A TypeSpec projekt inicializálása:

    tsp init
    
  5. Válaszoljon a következő kérdésekre a megadott válaszokkal:

    • Inicializál egy új projektet? Y
    • Kiválaszt egy projektsablont? Általános REST API
    • Projektnév megadása: Widgetek
    • Milyen emittereket szeretne használni?
      • OpenAPI 3.1-dokumentum
      • C# kiszolgálói csonkok

    A TypeSpec emitterek olyan kódtárak, amelyek különböző TypeSpec fordító API-kat használnak a TypeSpec fordítási folyamatának tükrözésére és összetevők létrehozására.

  6. A folytatás előtt várja meg, amíg az inicializálás befejeződik.

    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. Fordítsa le a projektet:

    tsp compile .
    

    Jótanács

    Az iteratív fejlesztéshez használja a watch üzemmódot a fájlmódosítások automatikus újrafordítására: tsp compile . --watch. A létrehozott kiszolgálót és sémát naprakészen tartja, amint módosítja main.tsp.

  8. A TypeSpec létrehozza az alapértelmezett projektet a következő két külön mappában ./tsp-output:

    • séma
    • kiszolgáló
  9. Nyissa meg a ./tsp-output/schema/openapi.yaml fájlt. Figyelje meg, hogy a néhány sor ./main.tsp több mint 200 sornyi OpenApi-specifikációt hozott létre Önnek.

  10. Nyissa meg a ./tsp-output/server/aspnet mappát. Figyelje meg, hogy az állványozott .NET-fájlok a következők:

    • ./generated/operations/IWidgets.cs A Widgets metódusok felületét határozza meg.
    • ./generated/controllers/WidgetsController.cs implementálja a widgetek metódusaiba való integrációt. Itt helyezzük el az üzleti logikát.
    • ./generated/models A Widget API modelljeit határozza meg.

TypeSpec emitterek konfigurálása

Az API-kiszolgáló generációjának konfigurálásához használja a TypeSpec fájlokat.

  1. Nyissa meg a tspconfig.yaml meglévő konfigurációt, és cserélje le a következő YAML-re:

    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"
    

    Ez a konfiguráció megjósol több olyan változást, amelyre szükségünk van egy teljes mértékben generált .NET API-kiszolgálóhoz.

    • emit-mocks: Hozza létre a kiszolgálóhoz szükséges összes projektfájlt.
    • use-swaggerui: Integrálja a Swagger felhasználói felületét, hogy böngészőbarát módon használhassa az API-t.
    • emitter-output-dir: Állítsa be a kimeneti könyvtárat a kiszolgáló- és az OpenApi-specifikáció generációjához is.
    • Hozzon létre mindent a ./server.
  2. A projekt újrafordítása:

    tsp compile .
    
  3. Váltás az új /server könyvtárra:

    cd server
    
  4. Hozzon létre egy alapértelmezett fejlesztői tanúsítványt, ha még nem rendelkezik ilyen tanúsítvánnyal:

    dotnet dev-certs https
    
  5. Futtassa a projektet:

    dotnet run
    

    Várja meg, amíg az értesítés megnyílik a böngészőben.

  6. Nyissa meg a böngészőt, és adja hozzá a Swagger felhasználói felületi útvonalát. /swagger

    Képernyőkép a Swagger felhasználói felületéről a Widgets API-val.

  7. Az alapértelmezett TypeSpec API és a kiszolgáló is működik.

Az alkalmazásfájl-struktúra ismertetése

A létrehozott kiszolgáló projektstruktúrája tartalmazza a .NET vezérlőalapú API-kiszolgálót, a projekt létrehozásához szükséges .NET-fájlokat és az Azure-integráció közbenső szoftverét.

├── appsettings.Development.json
├── appsettings.json
├── docs
├── generated
├── mocks
├── Program.cs
├── Properties
├── README.md
├── ServiceProject.csproj
└── wwwroot
  • Adja hozzá az üzleti logikát: ebben a példában kezdje a ./server/mocks/Widget.cs fájllal. A generált Widget.cs sablon metódusokat biztosít.
  • Frissítse a kiszolgálót: adjon hozzá minden adott kiszolgálókonfigurációt a kiszolgálóhoz ./program.cs.
  • Használja az OpenApi specifikációt: a TypeSpec létrehozta a OpenApi3.json fájlt a ./server/wwwroot fájlba, és elérhetővé tette a Swagger UI-ben a fejlesztés közben. Ez egy felhasználói felületet biztosít a specifikációhoz. Az API-t anélkül használhatja, hogy olyan kérési mechanizmust kell megadnia, mint a REST-ügyfél vagy a webes előtér.

Adatmegőrzés módosítása az Azure Cosmos DB no-sql-ra

Most, hogy az alapszintű Widget API-kiszolgáló működik, frissítse a kiszolgálót az Azure Cosmos DB-vel való együttműködésre egy állandó adattárhoz.

  1. A címtárban adja hozzá az ./serverAzure Cosmos DB-t a projekthez:

    dotnet add package Microsoft.Azure.Cosmos
    
  2. Adja hozzá az Azure Identity-kódtárat a hitelesítéshez az Azure-ban:

    dotnet add package Azure.Identity
    
  3. Frissítse a ./server/ServiceProject.csproj Cosmos DB integrációs beállításait:

    <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>
    
    • Az EnableSdkContainerSupport lehetővé teszi a .NET SDK beépített tároló buildtámogatásának (dotnet publish ––container) használatát Dockerfile írása nélkül.
    • A Newtonsoft.Json hozzáadja azt a Json .NET szerializálót, amelyet a Cosmos DB SDK a .NET-objektumok JSON-ra és JSON-ból való konvertálására használ.
  4. Hozzon létre egy új regisztrációs fájlt a ./azure/CosmosDbRegistration Cosmos DB-regisztráció kezeléséhez:

    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");
                }
            }
        }
    }
    

    Figyelje meg a végpont környezeti változót:

    var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"];
    
  5. Hozzon létre egy új Widget-osztályt, ./azure/WidgetsCosmos.cs hogy üzleti logikát biztosítson az Azure Cosmos DB-vel való integrációhoz az állandó tárolóhoz.

    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. Hozza létre a fájlt az ./server/services/CosmosDbInitializer.cs Azure-ban való hitelesítéshez:

    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. Frissítse a ./server/program.cs-t, hogy a Cosmos DB-t használja, és engedélyezze a Swagger UI használatát egy éles környezetben. Másolás a teljes fájlba:

    // 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. A projekt létrehozása:

    dotnet build
    

    A projekt most a Cosmos DB-integrációval épül fel. Hozzuk létre az üzembehelyezési szkripteket az Azure-erőforrások létrehozásához és a projekt üzembe helyezéséhez.

Üzembehelyezési infrastruktúra létrehozása

Hozza létre a megismételhető üzembe helyezéshez szükséges fájlokat az Azure Developer CLI-vel és a Bicep-sablonokkal.

  1. A TypeSpec projekt gyökerénél hozzon létre egy azure.yaml üzembehelyezési definíciós fájlt, és illessze be a következő forrást:

    # 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
    

    Figyelje meg, hogy ez a konfiguráció a létrehozott projekthelyre (./server) hivatkozik. Győződjön meg arról, hogy a ./tspconfig.yaml egyezik a ./azure.yaml-ben megadott helyszínnel.

  2. A TypeSpec projekt gyökerénél hozzon létre egy könyvtárat ./infra .

  3. Hozzon létre egy ./infra/main.bicepparam fájlt, és másolja az alábbiakba az üzembe helyezéshez szükséges paraméterek meghatározásához:

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

    Ez a paramlista biztosítja az üzembe helyezéshez szükséges minimális paramétereket.

  4. Hozzon létre egy ./infra/main.bicep fájlt, és másolja az alábbiakba a kiépítéshez és üzembe helyezéshez szükséges Azure-erőforrások meghatározásához:

    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
    

    A kimeneti változók lehetővé teszik a kiépített felhőerőforrások használatát a helyi fejlesztéssel.

  5. A containerAppsApp címke a serviceName változót (a api értékre állítva a fájl tetején) és a api-et, amit a ./azure.yaml-ben adtak meg, használja. Ez a kapcsolat tájékoztatja az Azure Developer CLI-t, hogy hol helyezze üzembe a .NET-projektet az Azure Container Apps üzemeltetési erőforrásában.

    ...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..
    

Projektstruktúra

A végső projektstruktúra tartalmazza a TypeSpec API-fájlokat, a Express.js kiszolgálót és az Azure üzembehelyezési fájljait:

├── infra
├── tsp-output
├── .gitignore
├── .azure.yaml
├── Dockerfile
├── main.tsp
├── package-lock.json
├── package.json
├── tspconfig.yaml
Area Fájlok/könyvtárak
TypeSpec main.tsp, tspconfig.yaml
Express.js kiszolgáló ./tsp-output/server/(beleértve a létrehozott fájlokat, például controllers/: , models/ServiceProject.csproj)
Azure Developer CLI üzembe helyezése ./azure.yaml,./infra/

Alkalmazás üzembe helyezése az Azure-ban

Ezt az alkalmazást az Azure Container Apps használatával telepítheti az Azure-ban:

  1. Hitelesítés az Azure Developer CLI-ben:

    azd auth login
    
  2. Üzembe helyezés az Azure Container Appsben az Azure Developer CLI használatával:

    azd up
    

Alkalmazás használata böngészőben

Az üzembe helyezést követően a következőt teheti:

  1. Nyissa meg a Swagger felhasználói felületet az API teszteléséhez itt: /swagger.
  2. Az egyes API-k Kipróbálás funkciójával widgeteket hozhat létre, olvashat, frissíthet és törölhet az API-n keresztül.

Az alkalmazás növelése

Most, hogy a teljes folyamat teljes körűen működik, folytassa az API elkészítését:

  • Ismerje meg a TypeSpec nyelvet, hogy további API-kat és API-rétegfunkciókat adhasson hozzá a ./main.tsp.
  • Adjon hozzá további emittereket , és konfigurálja a paramétereket a ./tspconfig.yaml.
  • Ahogy további funkciókat ad hozzá a TypeSpec-fájlokhoz, a kiszolgálóprojekt forráskódjával támogatja ezeket a módosításokat.
  • Továbbra is használjon jelszó nélküli hitelesítést az Azure Identity használatával.

Erőforrások tisztítása

Ha végzett ezzel a rövid útmutatóval, eltávolíthatja az Azure-erőforrásokat:

azd down

Vagy törölje az erőforráscsoportot közvetlenül az Azure Portalról.

Hibaelhárítás

A .NET 9 követelmény nem teljesül

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

Solution:

  1. Ellenőrizze a .NET-verziót: dotnet --version.
  2. Telepítse a .NET 9-et a dot.net oldalról.

HTTPS-tanúsítványhiba

Hiba:System.IO.IOException: The certificate generation failed futtatáskor dotnet dev-certs https

Solution:

  1. Megbízható a meglévő tanúsítvány: dotnet dev-certs https --trust.
  2. Ha ez nem sikerül, tisztítsa meg és hozza létre újra a következőt: dotnet dev-certs https --clean, majd dotnet dev-certs https.
  3. Windows rendszeren győződjön meg arról, hogy rendszergazdaként futtatja a terminált.

@typespec/http-server-csharp emitter-ütközés

Hiba:Emitter error: Dependency conflict futtatáskor tsp compile .

Solution:

  1. Ellenőrizze, hogy tspconfig.yaml tartalmazza mindkét emittert a emit: alatt.

    emit:
      - "@typespec/openapi3"
      - "@typespec/http-server-csharp"
    
  2. Gyorsítótár törlése: npm cache clean --force, majd próbálkozzon újra: tsp compile ..

A Cosmos DB hitelesítése sikertelen

Hiba:Azure.Identity.AuthenticationFailedException Vagy Connection string/key not set

Solution:

  1. Győződjön meg arról, hogy a környezeti változó be van állítva: echo $AZURE_COSMOS_ENDPOINT (macOS/Linux) vagy echo %AZURE_COSMOS_ENDPOINT% (Windows).
  2. Helyi fejlesztéshez állítsa be az értéket a Program.cs vagy egy .env fájlban.
  3. A termelési célra ellenőrizze, hogy a Tárolóalkalmazás titkos kulcsa konfigurálva van (lásd az "Üzembe helyezési infrastruktúra létrehozása" szakaszt).

A Swagger felhasználói felülete a 404-et mutatja

Hiba: A böngésző a következőt mutatja 404 Not Found/swagger

Solution:

  1. Ellenőrizze a Swagger-végpont konfigurációját a következő helyen Program.cs:

    c.SwaggerEndpoint("/openapi.yaml", "TypeSpec Generated OpenAPI Docs");
    c.RoutePrefix = "swagger";
    
  2. Győződjön meg arról openapi.yaml , hogy a wwwroot/ címtárban van.

  3. Újraépítés és újraindítás: dotnet clean && dotnet build && dotnet run.

Következő lépések