共用方式為


快速入門:使用 TypeSpec 和 .NET 建立新的 API 專案

在本快速入門中,您將瞭解如何使用 TypeSpec 來設計、產生及實作 RESTful API 應用程式。 TypeSpec 是一種開放原始碼語言,用於描述雲端服務 API,並針對多個平台產生客戶端和伺服器程序代碼。 遵循本快速入門,您將瞭解如何定義 API 合約一次,併產生一致的實作,協助您建置更多可維護且記錄良好的 API 服務。

在本快速入門中,您將:

  • 使用 TypeSpec 定義您的 API
  • 建立 API 伺服器應用程式
  • 整合 Azure Cosmos DB 以實現持久性儲存
  • 在本機執行及測試您的 API
  • 部署至 Azure Container Apps

Prerequisites

使用 TypeSpec 進行開發

TypeSpec 會以語言無關的方式定義您的 API,並針對多個平台產生 API 伺服器和用戶端連結庫。 此功能讓您可以:

  • 定義 API 合約只需一次即可
  • 產生一致的伺服器和用戶端程序代碼
  • 專注於實作商業規則,而不是 API 基礎結構

TypeSpec 提供 API 服務管理

  • API 定義語言
  • API 的伺服器端路由中間件
  • 取用 API 的用戶端程式庫

您提供用戶端要求和伺服器整合:

  • 在中間件中實作商業規則,例如適用於資料庫、記憶體和傳訊的 Azure 服務
  • 用於託管 API 的伺服器(在本機或 Azure 中)
  • 可重複布建和部署的部署腳本

建立新的 TypeSpec 應用程式

  1. 建立新的資料夾來保存 API 伺服器和 TypeSpec 檔案。

    mkdir my_typespec_quickstart
    cd my_typespec_quickstart
    
  2. 全域安裝 TypeSpec 編譯程式

    npm install -g @typespec/compiler
    
  3. 檢查 TypeSpec 是否已正確安裝:

    tsp --version
    
  4. 初始化 TypeSpec 專案:

    tsp init
    
  5. 回答下列提示,並提供答案:

    • 在這裡初始化新專案嗎? Y
    • 選取項目範本? 一般 REST API
    • 輸入項目名稱:小工具
    • 您要使用哪些排放器?
      • OpenAPI 3.1 檔
      • C# 伺服器存根

    TypeSpec 發出器 是一些利用各種 TypeSpec 編譯器 API 來對 TypeSpec 編譯過程進行分析並生成工件的函式庫。

  6. 等候初始化完成再繼續。

    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. 編譯專案:

    tsp compile .
    
  8. TypeSpec 會在 中 ./tsp-output產生預設專案,並建立兩個不同的資料夾:

    • 結構描述
    • 伺服器
  9. 開啟 ./tsp-output/schema/openapi.yaml 檔案。 請注意,為您產生的幾行 ./main.tsp 生成了超過 200 行的 OpenApi 規範。

  10. 開啟 ./tsp-output/server/aspnet 資料夾。 請注意,Scaffolded .NET 檔案包括:

    • ./generated/operations/IWidgets.cs 會定義 Widgets 方法的介面。
    • ./generated/controllers/WidgetsController.cs 會實現與小工具方法的整合。 這就是您放置商業邏輯的位置。
    • ./generated/models 定義 Widget API 的模型。

設定 TypeSpec 發射器

使用 TypeSpec 檔案來設定 API 伺服器產生。

  1. 開啟 tsconfig.yaml,並將現有的組態取代為下列 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"
    

    此組態將展示我們為完整生成的 .NET API 伺服器所需的數項變更:

    • emit-mocks:建立伺服器所需的所有項目檔。
    • use-swaggerui:整合 Swagger UI,以便您能透過更友善的瀏覽器方式使用 API。
    • emitter-output-dir:為伺服器產生和 OpenApi 規格產生設定輸出目錄。
    • 將所有內容產生至 ./server
  2. 重新編譯專案:

    tsp compile .
    
  3. 變更為新的 /server 目錄:

    cd server
    
  4. 如果您還沒有預設開發人員憑證,請建立:

    dotnet dev-certs https
    
  5. 執行專案:

    dotnet run
    

    等候通知後,於瀏覽器中開啟

  6. 開啟瀏覽器,並新增 Swagger UI 路由。 /swagger

    顯示 Swagger UI 與 Widgets API 的螢幕快照。

  7. 預設的 TypeSpec API 和伺服器都能夠運作。

瞭解應用程式檔案結構

所產生伺服器的項目結構包括 .NET 控制器型 API 伺服器、用於建置專案的 .NET 檔案,以及 Azure 整合的中間件。

├── appsettings.Development.json
├── appsettings.json
├── docs
├── generated
├── mocks
├── Program.cs
├── Properties
├── README.md
├── ServiceProject.csproj
└── wwwroot
  • 新增商業規則:在此範例中,從 ./server/mocks/Widget.cs 檔案開始。 自動生成的 Widget.cs 提供樣板方法。
  • 更新伺服器:將任何特定的伺服器組態新增至 ./program.cs
  • 使用 OpenApi 規格:TypeSpec 會在開發期間將 OpenApi3.json 檔案產生到 ./server/wwwroot 檔案中,並使其可供 Swagger UI 使用。 這將為您的規格提供使用者介面。 您可以與 API 互動,而不需要提供 REST 用戶端或 Web 前端等要求機制。

切換至 Azure Cosmos DB NoSQL 來實現資料持久性

現在,基本 Widget API 伺服器正在運作,請更新伺服器以使用 Azure Cosmos DB 作為永續性數據存放區。

  1. ./server 目錄中,將 Azure Cosmos DB 新增至專案:

    dotnet add package Microsoft.Azure.Cosmos
    
  2. 新增 Azure 身分識別連結庫向 Azure 進行驗證

    dotnet add package Azure.Identity
    
  3. 更新 Cosmos DB 整合設定的 ./server/ServiceProject.csproj

    <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>
    
    • EnableSdkContainerSupport 可讓您使用 .NET SDK 的內建容器組建支援(dotnet publish ––container),而不需撰寫 Dockerfile。
    • Newtonsoft.Json 新增了一個 Json .NET 序列化工具,Cosmos DB SDK 使用它來進行 .NET 物件與 JSON 之間的轉換。
  4. 建立新的註冊檔案, ./azure/CosmosDbRegistration 以管理 Cosmos DB 註冊:

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

    請注意端點的環境變數:

    var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"];
    
  5. 建立新的 Widget 類別,./azure/WidgetsCosmos.cs,提供業務邏輯,以便與 Azure Cosmos DB 整合,用於您的持續性儲存。

    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. 建立 ./server/services/CosmosDbInitializer.cs 檔案以在 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. 更新./server/program.cs以使用 Cosmos DB,並使 Swagger UI 可以用於生產部署。 複製整個檔案:

    // 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. 建置專案:

    dotnet build
    

    項目現在會使用 Cosmos DB 整合來建置。 讓我們建立部署腳本來建立 Azure 資源並部署專案。

建立部署基礎結構

使用 Azure 開發人員 CLI 和 Bicep 範本建立可重複部署所需的檔案。

  1. 在 TypeSpec 專案的根目錄中,建立 azure.yaml 部署定義檔,並貼上下列來源:

    # 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
    

    請注意,此組態會參考產生的專案位置 (./server)。 請確定 ./tspconfig.yaml 符合 中指定的 ./azure.yaml位置。

  2. 在 TypeSpec 專案的根目錄中,建立 ./infra 目錄。

  3. 建立一個 ./infra/main.bicepparam 檔案,並將以下內容複製進去,以定義我們部署所需的參數:

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

    此參數清單提供此部署所需的最低參數。

  4. 建立 ./infra/main.bicep 檔案並將以下內容複製到檔案中,以定義用於布建和部署的 Azure 資源:

    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
    

    輸出變數可讓您搭配本機開發使用布建的雲端資源。

  5. containerAppsApp 標記會使用 serviceName 變數(在檔案頂端設定為 api)以及在 api 中指定的 ./azure.yaml。 此聯機會告知 Azure 開發人員 CLI 將 .NET 專案部署至 Azure Container Apps 主控資源的位置。

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

專案結構

最終的項目結構包括 TypeSpec API 檔案、Express.js 伺服器和 Azure 部署檔案:

├── infra
├── tsp-output
├── .gitignore
├── .azure.yaml
├── Dockerfile
├── main.tsp
├── package-lock.json
├── package.json
├── tspconfig.yaml
Area 檔案/目錄
TypeSpec main.tsptspconfig.yaml
Express.js 伺服器 ./tsp-output/server/ (包括產生的檔案,例如 controllers/models/ServiceProject.csproj
Azure 開發人員 CLI 部署 ./azure.yaml./infra/

將應用程式部署至 Azure

您可以使用 Azure Container Apps 將此應用程式部署至 Azure:

  1. 向 Azure 開發人員 CLI 進行驗證:

    azd auth login
    
  2. 使用 Azure 開發人員 CLI 部署至 Azure Container Apps:

    azd up
    

在瀏覽器中使用應用程式

部署之後,您可以:

  1. 使用 Swagger UI 來在 /swagger 測試您的 API。
  2. 使用每個 API 上的 [立即試用] 功能,透過 API 建立、讀取、更新和刪除小工具。

拓展您的應用程式

既然您已讓整個端對端程式正常運作,請繼續建置您的 API:

  • 深入瞭解 TypeSpec 語言 ,以在 中 ./main.tsp新增更多 API 和 API 層功能。
  • 請新增其他 發出器,並在./tspconfig.yaml中設定其參數。
  • 當您在 TypeSpec 檔案中新增更多功能時,請在伺服器專案中使用原始程式碼支援這些變更。
  • 繼續使用 Azure 身分識別的無密碼驗證

清理資源

完成本快速入門后,您可以移除 Azure 資源:

azd down

或者直接從 Azure 入口網站刪除資源群組。

後續步驟