快速入门:使用 TypeSpec 和 .NET 创建新的 API 项目

本快速入门介绍如何使用 TypeSpec 设计、生成和实现 RESTful API 应用程序。 TypeSpec 是一种开源语言,用于描述云服务 API,并为多个平台生成客户端和服务器代码。 按照本快速入门作,了解如何定义 API 协定一次并生成一致的实现,从而帮助你构建更易维护且记录良好的 API 服务。

在本快速入门中,请执行以下操作:

  • 使用 TypeSpec 定义 API
  • 创建 API 服务器应用程序
  • 将 Azure Cosmos DB 集成以实现持久存储
  • 在本地运行和测试 API
  • 部署到 Azure 容器应用

先决条件

使用 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
    • 输入项目名称:Widgets
    • 要使用的发射器是什么?
      • 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 文件夹。 请注意,基架的 .NET 文件包括:

    • ./generated/operations/IWidgets.cs 定义 Widgets 方法的接口。
    • ./generated/controllers/WidgetsController.cs 实现与 Widgets 方法的集成。 这就是放置业务逻辑的位置。
    • ./generated/models 定义小组件 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 及 Widget 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。 这为你的规范提供了一个用户界面。 无需提供 REST 客户端或 Web 前端等请求机制即可与 API 交互。

将数据持久性更改为 Azure Cosmos DB NoSQL

现在,基本小组件 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 格式,或从 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. 创建一个新的小组件类,./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 容器应用托管资源的位置。

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

将应用程序部署到 Azure

可以使用 Azure 容器应用将此应用程序部署到 Azure:

  1. 向 Azure 开发人员 CLI 进行身份验证:

    azd auth login
    
  2. 使用 Azure 开发人员 CLI 部署到 Azure 容器应用:

    azd up
    

在浏览器中使用应用程序

部署后,可以:

  1. 访问 Swagger UI 以在 /swagger 测试您的 API。
  2. 使用立即试用功能在每个 API 上通过 API 创建、读取、更新和删除小组件。

扩展应用程序

完成整个端到端过程后,请继续生成 API:

  • 详细了解 TypeSpec 语言,以便在 ./main.tsp 中添加更多 API 和 API 层功能。
  • 添加额外的发射器并在./tspconfig.yaml中配置其参数。
  • 在 TypeSpec 文件中添加更多功能时,请在服务器项目中使用源代码支持这些更改。
  • 继续通过 Azure 标识使用 无密码身份验证

清理资源

完成本快速入门后,可以删除 Azure 资源:

azd down

或者直接从 Azure 门户删除资源组。

后续步骤