Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
In deze quickstart leert u hoe u TypeSpec gebruikt om een RESTful API-toepassing te ontwerpen, genereren en implementeren. TypeSpec is een opensource-taal voor het beschrijven van cloudservice-API's en genereert client- en servercode voor meerdere platforms. Door deze quickstart te volgen, leert u hoe u uw API-contract eenmaal definieert en consistente implementaties genereert, zodat u beter onderhoudbare en goed gedocumenteerde API-services kunt bouwen.
U gebruikt de codegenerator van TypeSpec om een ASP.NET Core-server te maken met routering en Swagger UI, deze te verbinden met Azure Cosmos DB voor persistentie en te implementeren in Azure Container Apps.
Zie Overzicht van TypeSpec voor context over de rol van TypeSpec in API-ontwikkeling.
Tijd om te voltooien: 20-25 minuten
In deze snelstart, gaat u het volgende doen:
- Uw API definiëren met Behulp van TypeSpec
- Een API-servertoepassing maken
- Azure Cosmos DB integreren voor permanente opslag
- Uw API lokaal uitvoeren en testen
- Implementeren in Azure Container Apps
Prerequisites
- Een actief Azure-account. Maak gratis een account als u er nog geen hebt.
- .NET 9 SDK
- Node.js LTS : vereist voor de TypeSpec CLI en package manager.
- Visual Studio Code met de volgende extensies:
- TypeSpec-extensie
- Optioneel: Implementatie met Azure Developer CLI
Ontwikkelen met TypeSpec
TypeSpec definieert uw API op een taalagnostische manier en genereert de API-server en clientbibliotheek voor meerdere platforms. Met deze functionaliteit kunt u het volgende doen:
- Uw API-contract eenmaal definiëren
- Consistente server- en clientcode genereren
- Focus op het implementeren van bedrijfslogica in plaats van API-infrastructuur
TypeSpec biedt API-servicebeheer:
- API-definitietaal
- Middleware voor routering aan de serverzijde voor API
- Clientbibliotheken voor het verbruik van API
U verstrekt clientaanvragen en serverintegraties:
- Bedrijfslogica implementeren in middleware zoals Azure-services voor databases, opslag en berichten
- Hostingserver voor uw API (lokaal of in Azure)
- Implementatiescripts voor herhaalbare inrichting en implementatie
Een nieuwe TypeSpec-toepassing maken
Maak een nieuwe map voor de API-server en TypeSpec-bestanden.
mkdir my_typespec_quickstart cd my_typespec_quickstartInstalleer de TypeSpec-compiler globaal:
npm install -g @typespec/compilerControleer of TypeSpec juist is geïnstalleerd:
tsp --versionInitialiseer het TypeSpec-project:
tsp initBeantwoord de volgende prompts met de opgegeven antwoorden:
- Initialiseer hier een nieuw project? Y
- Selecteer een projectsjabloon? Algemene REST API
- Voer een projectnaam in: Widgets
- Welke emitters wilt u gebruiken?
- OpenAPI 3.1-document
- C# serverstubs
TypeSpec-emitters zijn bibliotheken die gebruikmaken van verschillende TypeSpec-compiler-API's om te weerspiegelen over het TypeSpec-compilatieproces en artefacten te genereren.
Wacht totdat de initialisatie is voltooid voordat u doorgaat.
Run tsp compile . to build the project. Please review the following messages from emitters: @typespec/http-server-csharp: Generated ASP.Net services require dotnet 9: https://dotnet.microsoft.com/download Create an ASP.Net service project for your TypeSpec: > npx hscs-scaffold . --use-swaggerui --overwrite More information on getting started: https://aka.ms/tsp/hscs/startCompileer het project:
tsp compile .Aanbeveling
Voor iteratieve ontwikkeling gebruikt u de controlemodus om automatisch opnieuw te compileren bij bestandswijzigingen:
tsp compile . --watchHierdoor blijven de gegenereerde server en het schema up-to-date terwijl u deze wijzigtmain.tsp.TypeSpec genereert het standaardproject in
./tsp-output, waarbij twee afzonderlijke mappen worden gemaakt:- schema
- server
Open het
./tsp-output/schema/openapi.yaml-bestand. U ziet dat de weinige regels in./main.tspmeer dan 200 regels OpenAPI-specificatie hebben gegenereerd.Open de map
./tsp-output/server/aspnet. U ziet dat de gescaffolde .NET-bestanden het volgende bevatten:-
./generated/operations/IWidgets.csdefinieert de interface voor de widgetsmethoden. -
./generated/controllers/WidgetsController.csimplementeert de integratie met de widgetsmethoden. Hier plaatst u uw bedrijfslogica. -
./generated/modelsdefinieert de modellen voor de Widget-API.
-
TypeSpec emitters configureren
Gebruik de TypeSpec-bestanden om het genereren van de API-server te configureren.
Open de
tspconfig.yamlbestaande configuratie en vervang deze door de volgende YAML:emit: - "@typespec/openapi3" - "@typespec/http-server-csharp" options: "@typespec/openapi3": emitter-output-dir: "{cwd}/server/wwwroot" openapi-versions: - 3.1.0 "@typespec/http-server-csharp": emitter-output-dir: "{cwd}/server/" use-swaggerui: true overwrite: true emit-mocks: "mocks-and-project-files"Deze configuratie projecteert verschillende wijzigingen die we nodig hebben voor een volledig gegenereerde .NET API-server:
-
emit-mocks: Maak alle projectbestanden die nodig zijn voor de server. -
use-swaggerui: Integreer de Swagger-gebruikersinterface zodat u de API op een browservriendelijke manier kunt gebruiken. -
emitter-output-dir: Stel de uitvoermap in voor zowel het genereren van servers als het genereren van OpenApi-specificatie. - Genereer alles in
./server.
-
Het project opnieuw compileren:
tsp compile .Ga naar de nieuwe
/serverdirectory:cd serverMaak een standaardcertificaat voor ontwikkelaars als u er nog geen hebt:
dotnet dev-certs httpsVoer het project uit:
dotnet runWacht totdat de melding is geopend in de browser.
Open de browser en voeg de Swagger UI-route toe.
/swaggerDe standaard TypeSpec-API en de server werken allebei.
Inzicht in de structuur van toepassingsbestanden
De projectstructuur voor de gegenereerde server bevat de API-server op basis van de .NET-controller, de .NET-bestanden voor het bouwen van het project en de middleware voor uw Azure-integratie.
├── appsettings.Development.json
├── appsettings.json
├── docs
├── generated
├── mocks
├── Program.cs
├── Properties
├── README.md
├── ServiceProject.csproj
└── wwwroot
-
Voeg uw bedrijfslogica toe: begin in dit voorbeeld met het
./server/mocks/Widget.csbestand. De gegenereerdeWidget.csbiedt standaardmethoden. -
Werk de server bij: voeg eventuele specifieke serverconfiguraties toe aan
./program.cs. -
Gebruik de OpenApi-specificatie: de TypeSpec heeft het OpenApi3.json bestand gegenereerd in het
./server/wwwrootbestand geplaatst en het beschikbaar gemaakt voor Swagger UI tijdens de ontwikkeling. Dit biedt een gebruikersinterface voor uw specificatie. U kunt communiceren met uw API zonder dat u een aanvraagmechanisme hoeft op te geven, zoals een REST-client of webfront-end.
Persistentie wijzigen in Azure Cosmos DB no-sql
Nu de basiswidget-API-server werkt, werkt u de server bij om te werken met Azure Cosmos DB voor een permanent gegevensarchief.
Voeg
./servertoe aan het project in de map:dotnet add package Microsoft.Azure.CosmosVoeg de Azure Identity-bibliotheek toe om te verifiëren bij Azure:
dotnet add package Azure.IdentityWerk de
./server/ServiceProject.csprojintegratie-instellingen voor Cosmos DB bij:<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> ... existing settings ... <EnableSdkContainerSupport>true</EnableSdkContainerSupport> </PropertyGroup> <ItemGroup> ... existing settings ... <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> </ItemGroup> </Project>- Met EnableSdkContainerSupport kunt u de ingebouwde ondersteuning voor containerbuilding van de .NET SDK (dotnet publish ––container) gebruiken zonder een Dockerfile te schrijven.
- Newtonsoft.Json voegt de Json .NET-serializer toe die door de Cosmos DB SDK wordt gebruikt om uw .NET-objecten te converteren naar en van JSON.
Maak een nieuw registratiebestand
./azure/CosmosDbRegistrationom de Cosmos DB-registratie te beheren:using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Configuration; using System; using System.Threading.Tasks; using Azure.Identity; using DemoService; namespace WidgetService.Service { /// <summary> /// Registration class for Azure Cosmos DB services and implementations /// </summary> public static class CosmosDbRegistration { /// <summary> /// Registers the Cosmos DB client and related services for dependency injection /// </summary> /// <param name="builder">The web application builder</param> public static void RegisterCosmosServices(this WebApplicationBuilder builder) { // Register the HttpContextAccessor for accessing the HTTP context builder.Services.AddHttpContextAccessor(); // Get configuration settings var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"]; // Validate configuration ValidateCosmosDbConfiguration(cosmosEndpoint); // Configure Cosmos DB client options var cosmosClientOptions = new CosmosClientOptions { SerializerOptions = new CosmosSerializationOptions { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase }, ConnectionMode = ConnectionMode.Direct }; builder.Services.AddSingleton(serviceProvider => { var credential = new DefaultAzureCredential(); // Create Cosmos client with token credential authentication return new CosmosClient(cosmosEndpoint, credential, cosmosClientOptions); }); // Initialize Cosmos DB if needed builder.Services.AddHostedService<CosmosDbInitializer>(); // Register WidgetsCosmos implementation of IWidgets builder.Services.AddScoped<IWidgets, WidgetsCosmos>(); } /// <summary> /// Validates the Cosmos DB configuration settings /// </summary> /// <param name="cosmosEndpoint">The Cosmos DB endpoint</param> /// <exception cref="ArgumentException">Thrown when configuration is invalid</exception> private static void ValidateCosmosDbConfiguration(string cosmosEndpoint) { if (string.IsNullOrEmpty(cosmosEndpoint)) { throw new ArgumentException("Cosmos DB Endpoint must be specified in configuration"); } } } }Let op de omgevingsvariabele voor het eindpunt:
var cosmosEndpoint = builder.Configuration["Configuration:AzureCosmosDb:Endpoint"];Maak een nieuwe widgetklasse om
./azure/WidgetsCosmos.csbedrijfslogica te bieden die kan worden geïntegreerd met Azure Cosmos DB voor uw permanente opslag.using System; using System.Net; using System.Threading.Tasks; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Linq; // Use generated models and operations using DemoService; namespace WidgetService.Service { /// <summary> /// Implementation of the IWidgets interface that uses Azure Cosmos DB for persistence /// </summary> public class WidgetsCosmos : IWidgets { private readonly CosmosClient _cosmosClient; private readonly ILogger<WidgetsCosmos> _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly string _databaseName = "WidgetDb"; private readonly string _containerName = "Widgets"; /// <summary> /// Initializes a new instance of the WidgetsCosmos class. /// </summary> /// <param name="cosmosClient">The Cosmos DB client instance</param> /// <param name="logger">Logger for diagnostic information</param> /// <param name="httpContextAccessor">Accessor for the HTTP context</param> public WidgetsCosmos( CosmosClient cosmosClient, ILogger<WidgetsCosmos> logger, IHttpContextAccessor httpContextAccessor) { _cosmosClient = cosmosClient; _logger = logger; _httpContextAccessor = httpContextAccessor; } /// <summary> /// Gets a reference to the Cosmos DB container for widgets /// </summary> private Container WidgetsContainer => _cosmosClient.GetContainer(_databaseName, _containerName); /// <summary> /// Lists all widgets in the database /// </summary> /// <returns>Array of Widget objects</returns> public async Task<WidgetList> ListAsync() { try { var queryDefinition = new QueryDefinition("SELECT * FROM c"); var widgets = new List<Widget>(); using var iterator = WidgetsContainer.GetItemQueryIterator<Widget>(queryDefinition); while (iterator.HasMoreResults) { var response = await iterator.ReadNextAsync(); widgets.AddRange(response.ToList()); } // Create and return a WidgetList instead of Widget[] return new WidgetList { Items = widgets.ToArray() }; } catch (Exception ex) { _logger.LogError(ex, "Error listing widgets from Cosmos DB"); throw new Error(500, "Failed to retrieve widgets from database"); } } /// <summary> /// Retrieves a specific widget by ID /// </summary> /// <param name="id">The ID of the widget to retrieve</param> /// <returns>The retrieved Widget</returns> public async Task<Widget> ReadAsync(string id) { try { var response = await WidgetsContainer.ReadItemAsync<Widget>( id, new PartitionKey(id)); return response.Resource; } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { _logger.LogWarning("Widget with ID {WidgetId} not found", id); throw new Error(404, $"Widget with ID '{id}' not found"); } catch (Exception ex) { _logger.LogError(ex, "Error reading widget {WidgetId} from Cosmos DB", id); throw new Error(500, "Failed to retrieve widget from database"); } } /// <summary> /// Creates a new widget from the provided Widget object /// </summary> /// <param name="body">The Widget object to store in the database</param> /// <returns>The created Widget</returns> public async Task<Widget> CreateAsync(Widget body) { try { // Validate the Widget if (body == null) { throw new Error(400, "Widget data cannot be null"); } if (string.IsNullOrEmpty(body.Id)) { throw new Error(400, "Widget must have an Id"); } if (body.Color != "red" && body.Color != "blue") { throw new Error(400, "Color must be 'red' or 'blue'"); } // Save the widget to Cosmos DB var response = await WidgetsContainer.CreateItemAsync( body, new PartitionKey(body.Id)); _logger.LogInformation("Created widget with ID {WidgetId}", body.Id); return response.Resource; } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.Conflict) { _logger.LogError(ex, "Widget with ID {WidgetId} already exists", body.Id); throw new Error(409, $"Widget with ID '{body.Id}' already exists"); } catch (Exception ex) when (!(ex is Error)) { _logger.LogError(ex, "Error creating widget in Cosmos DB"); throw new Error(500, "Failed to create widget in database"); } } /// <summary> /// Updates an existing widget with properties specified in the patch document /// </summary> /// <param name="id">The ID of the widget to update</param> /// <param name="body">The WidgetMergePatchUpdate object containing properties to update</param> /// <returns>The updated Widget</returns> public async Task<Widget> UpdateAsync(string id, TypeSpec.Http.WidgetMergePatchUpdate body) { try { // Validate input parameters if (body == null) { throw new Error(400, "Update data cannot be null"); } if (body.Color != null && body.Color != "red" && body.Color != "blue") { throw new Error(400, "Color must be 'red' or 'blue'"); } // First check if the item exists Widget existingWidget; try { var response = await WidgetsContainer.ReadItemAsync<Widget>( id, new PartitionKey(id)); existingWidget = response.Resource; } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { _logger.LogWarning("Widget with ID {WidgetId} not found for update", id); throw new Error(404, $"Widget with ID '{id}' not found"); } // Apply the patch updates only where properties are provided bool hasChanges = false; if (body.Weight.HasValue) { existingWidget.Weight = body.Weight.Value; hasChanges = true; } if (body.Color != null) { existingWidget.Color = body.Color; hasChanges = true; } // Only perform the update if changes were made if (hasChanges) { // Use ReplaceItemAsync for the update var updateResponse = await WidgetsContainer.ReplaceItemAsync( existingWidget, id, new PartitionKey(id)); _logger.LogInformation("Updated widget with ID {WidgetId}", id); return updateResponse.Resource; } // If no changes, return the existing widget _logger.LogInformation("No changes to apply for widget with ID {WidgetId}", id); return existingWidget; } catch (Error) { // Rethrow Error exceptions throw; } catch (Exception ex) { _logger.LogError(ex, "Error updating widget {WidgetId} in Cosmos DB", id); throw new Error(500, "Failed to update widget in database"); } } /// <summary> /// Deletes a widget by its ID /// </summary> /// <param name="id">The ID of the widget to delete</param> public async Task DeleteAsync(string id) { try { await WidgetsContainer.DeleteItemAsync<Widget>(id, new PartitionKey(id)); _logger.LogInformation("Deleted widget with ID {WidgetId}", id); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { _logger.LogWarning("Widget with ID {WidgetId} not found for deletion", id); throw new Error(404, $"Widget with ID '{id}' not found"); } catch (Exception ex) { _logger.LogError(ex, "Error deleting widget {WidgetId} from Cosmos DB", id); throw new Error(500, "Failed to delete widget from database"); } } /// <summary> /// Analyzes a widget by ID and returns a simplified analysis result /// </summary> /// <param name="id">The ID of the widget to analyze</param> /// <returns>An AnalyzeResult containing the analysis of the widget</returns> public async Task<AnalyzeResult> AnalyzeAsync(string id) { try { // First retrieve the widget from the database Widget widget; try { var response = await WidgetsContainer.ReadItemAsync<Widget>( id, new PartitionKey(id)); widget = response.Resource; } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { _logger.LogWarning("Widget with ID {WidgetId} not found for analysis", id); throw new Error(404, $"Widget with ID '{id}' not found"); } // Create the analysis result var result = new AnalyzeResult { Id = widget.Id, Analysis = $"Weight: {widget.Weight}, Color: {widget.Color}" }; _logger.LogInformation("Analyzed widget with ID {WidgetId}", id); return result; } catch (Error) { // Rethrow Error exceptions throw; } catch (Exception ex) { _logger.LogError(ex, "Error analyzing widget {WidgetId} from Cosmos DB", id); throw new Error(500, "Failed to analyze widget from database"); } } } }Maak het
./server/services/CosmosDbInitializer.csbestand om te verifiëren bij Azure:using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace WidgetService.Service { /// <summary> /// Hosted service that initializes Cosmos DB resources on application startup /// </summary> public class CosmosDbInitializer : IHostedService { private readonly CosmosClient _cosmosClient; private readonly ILogger<CosmosDbInitializer> _logger; private readonly IConfiguration _configuration; private readonly string _databaseName; private readonly string _containerName = "Widgets"; public CosmosDbInitializer(CosmosClient cosmosClient, ILogger<CosmosDbInitializer> logger, IConfiguration configuration) { _cosmosClient = cosmosClient; _logger = logger; _configuration = configuration; _databaseName = _configuration["CosmosDb:DatabaseName"] ?? "WidgetDb"; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Ensuring Cosmos DB database and container exist..."); try { // Create database if it doesn't exist var databaseResponse = await _cosmosClient.CreateDatabaseIfNotExistsAsync( _databaseName, cancellationToken: cancellationToken); _logger.LogInformation("Database {DatabaseName} status: {Status}", _databaseName, databaseResponse.StatusCode == System.Net.HttpStatusCode.Created ? "Created" : "Already exists"); // Create container if it doesn't exist (using id as partition key) var containerResponse = await databaseResponse.Database.CreateContainerIfNotExistsAsync( new ContainerProperties { Id = _containerName, PartitionKeyPath = "/id" }, throughput: 400, // Minimum RU/s cancellationToken: cancellationToken); _logger.LogInformation("Container {ContainerName} status: {Status}", _containerName, containerResponse.StatusCode == System.Net.HttpStatusCode.Created ? "Created" : "Already exists"); } catch (Exception ex) { _logger.LogError(ex, "Error initializing Cosmos DB"); throw; } } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } }Werk de
./server/program.csbij om Cosmos DB te gebruiken en sta toe dat de Swagger-gebruikersinterface kan worden gebruikt in een productie-uitrol. Kopiëren in het hele bestand:// Generated by @typespec/http-server-csharp // <auto-generated /> #nullable enable using TypeSpec.Helpers; using WidgetService.Service; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(options => { options.Filters.Add<HttpServiceExceptionFilter>(); }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Replace original registration with the Cosmos DB one CosmosDbRegistration.RegisterCosmosServices(builder); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } // Swagger UI is always available app.UseSwagger(); app.UseSwaggerUI(c => { c.DocumentTitle = "TypeSpec Generated OpenAPI Viewer"; c.SwaggerEndpoint("/openapi.yaml", "TypeSpec Generated OpenAPI Docs"); c.RoutePrefix = "swagger"; }); app.UseHttpsRedirection(); app.UseStaticFiles(); app.Use(async (context, next) => { context.Request.EnableBuffering(); await next(); }); app.MapGet("/openapi.yaml", async (HttpContext context) => { var externalFilePath = "wwwroot/openapi.yaml"; if (!File.Exists(externalFilePath)) { context.Response.StatusCode = StatusCodes.Status404NotFound; await context.Response.WriteAsync("OpenAPI spec not found."); return; } context.Response.ContentType = "application/json"; await context.Response.SendFileAsync(externalFilePath); }); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();Bouw het project:
dotnet buildHet project bouwt nu met Cosmos DB-integratie. We gaan de implementatiescripts maken om de Azure-resources te maken en het project te implementeren.
Implementatie-infrastructuur maken
Maak de bestanden die nodig zijn voor een herhaalbare implementatie met Azure Developer CLI en Bicep-sjablonen.
Maak in de hoofdmap van uw TypeSpec-project een
azure.yamlimplementatiedefinitiebestand en plak de volgende brontekst:# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json name: azure-typespec-scaffold-dotnet metadata: template: azd-init@1.14.0 services: api: project: ./server host: containerapp language: dotnet pipeline: provider: githubU ziet dat deze configuratie verwijst naar de gegenereerde projectlocatie (
./server). Zorg ervoor dat./tspconfig.yamldeze overeenkomt met de locatie die is opgegeven in./azure.yaml.Maak een
./inframap in de hoofdmap van het TypeSpec-project.Maak een
./infra/main.bicepparambestand en kopieer het volgende om de parameters te definiëren die we nodig hebben voor implementatie:using './main.bicep' param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'dev') param location = readEnvironmentVariable('AZURE_LOCATION', 'eastus2') param deploymentUserPrincipalId = readEnvironmentVariable('AZURE_PRINCIPAL_ID', '')Deze parameterslijst bevat de minimale parameters die nodig zijn voor deze implementatie.
Maak een
./infra/main.bicepbestand en kopieer het volgende om de Azure-resources voor inrichting en implementatie te definiëren:metadata description = 'Bicep template for deploying a GitHub App using Azure Container Apps and Azure Container Registry.' targetScope = 'resourceGroup' param serviceName string = 'api' var databaseName = 'WidgetDb' var containerName = 'Widgets' @minLength(1) @maxLength(64) @description('Name of the environment that can be used as part of naming resource convention') param environmentName string @minLength(1) @description('Primary location for all resources') param location string @description('Id of the principal to assign database and application roles.') param deploymentUserPrincipalId string = '' var resourceToken = toLower(uniqueString(resourceGroup().id, environmentName, location)) var tags = { 'azd-env-name': environmentName repo: 'https://github.com/typespec' } module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = { name: 'user-assigned-identity' params: { name: 'identity-${resourceToken}' location: location tags: tags } } module cosmosDb 'br/public:avm/res/document-db/database-account:0.8.1' = { name: 'cosmos-db-account' params: { name: 'cosmos-db-nosql-${resourceToken}' location: location locations: [ { failoverPriority: 0 locationName: location isZoneRedundant: false } ] tags: tags disableKeyBasedMetadataWriteAccess: true disableLocalAuth: true networkRestrictions: { publicNetworkAccess: 'Enabled' ipRules: [] virtualNetworkRules: [] } capabilitiesToAdd: [ 'EnableServerless' ] sqlRoleDefinitions: [ { name: 'nosql-data-plane-contributor' dataAction: [ 'Microsoft.DocumentDB/databaseAccounts/readMetadata' 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' ] } ] sqlRoleAssignmentsPrincipalIds: union( [ managedIdentity.outputs.principalId ], !empty(deploymentUserPrincipalId) ? [deploymentUserPrincipalId] : [] ) sqlDatabases: [ { name: databaseName containers: [ { name: containerName paths: [ '/id' ] } ] } ] } } module containerRegistry 'br/public:avm/res/container-registry/registry:0.5.1' = { name: 'container-registry' params: { name: 'containerreg${resourceToken}' location: location tags: tags acrAdminUserEnabled: false anonymousPullEnabled: true publicNetworkAccess: 'Enabled' acrSku: 'Standard' } } var containerRegistryRole = subscriptionResourceId( 'Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec' ) module registryUserAssignment 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.1' = if (!empty(deploymentUserPrincipalId)) { name: 'container-registry-role-assignment-push-user' params: { principalId: deploymentUserPrincipalId resourceId: containerRegistry.outputs.resourceId roleDefinitionId: containerRegistryRole } } module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.7.0' = { name: 'log-analytics-workspace' params: { name: 'log-analytics-${resourceToken}' location: location tags: tags } } module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.8.0' = { name: 'container-apps-env' params: { name: 'container-env-${resourceToken}' location: location tags: tags logAnalyticsWorkspaceResourceId: logAnalyticsWorkspace.outputs.resourceId zoneRedundant: false } } module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = { name: 'container-apps-app' params: { name: 'container-app-${resourceToken}' environmentResourceId: containerAppsEnvironment.outputs.resourceId location: location tags: union(tags, { 'azd-service-name': serviceName }) ingressTargetPort: 8080 ingressExternal: true ingressTransport: 'auto' stickySessionsAffinity: 'sticky' scaleMaxReplicas: 1 scaleMinReplicas: 1 corsPolicy: { allowCredentials: true allowedOrigins: [ '*' ] } managedIdentities: { systemAssigned: false userAssignedResourceIds: [ managedIdentity.outputs.resourceId ] } secrets: { secureList: [ { name: 'azure-cosmos-db-nosql-endpoint' value: cosmosDb.outputs.endpoint } { name: 'user-assigned-managed-identity-client-id' value: managedIdentity.outputs.clientId } ] } containers: [ { image: 'mcr.microsoft.com/dotnet/samples:aspnetapp-9.0' name: serviceName resources: { cpu: '0.25' memory: '.5Gi' } env: [ { name: 'CONFIGURATION__AZURECOSMOSDB__ENDPOINT' secretRef: 'azure-cosmos-db-nosql-endpoint' } { name: 'AZURE_CLIENT_ID' secretRef: 'user-assigned-managed-identity-client-id' } ] } ] } } output CONFIGURATION__AZURECOSMOSDB__ENDPOINT string = cosmosDb.outputs.endpoint output CONFIGURATION__AZURECOSMOSDB__DATABASENAME string = databaseName output CONFIGURATION__AZURECOSMOSDB__CONTAINERNAME string = containerName output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServerMet de uitvoervariabelen kunt u de ingerichte cloudresources gebruiken met uw lokale ontwikkeling.
De containerAppsApp-tag maakt gebruik van de serviceName-variabele (ingesteld op
apiboven aan het bestand) en deapiopgegeven in./azure.yaml. Deze verbinding vertelt de Azure Developer CLI waar het .NET-project moet worden geïmplementeerd in de Azure Container Apps-hostingresource....bicep... module containerAppsApp 'br/public:avm/res/app/container-app:0.9.0' = { name: 'container-apps-app' params: { name: 'container-app-${resourceToken}' environmentResourceId: containerAppsEnvironment.outputs.resourceId location: location tags: union(tags, { 'azd-service-name': serviceName }) <--------- `API` ...bicep..
Projectstructuur
De uiteindelijke projectstructuur bevat de TypeSpec API-bestanden, de Express.js-server en de Azure-implementatiebestanden:
├── infra
├── tsp-output
├── .gitignore
├── .azure.yaml
├── Dockerfile
├── main.tsp
├── package-lock.json
├── package.json
├── tspconfig.yaml
| Area | Bestanden/mappen |
|---|---|
| TypeSpec |
main.tsp, tspconfig.yaml |
| Express.js server |
./tsp-output/server/ (bevat gegenereerde bestanden zoals controllers/, models/, ServiceProject.csproj) |
| Azure Developer CLI-implementatie |
./azure.yaml,./infra/ |
Toepassing implementeren in Azure
U kunt deze toepassing implementeren in Azure met behulp van Azure Container Apps:
Verifiëren bij de Azure Developer CLI:
azd auth loginImplementeren in Azure Container Apps met behulp van de Azure Developer CLI:
azd up
Toepassing gebruiken in browser
Zodra de implementatie is uitgevoerd, kunt u het volgende doen:
- Open de Swagger-gebruikersinterface om uw API te testen op
/swagger. - Gebruik de functie Nu uitproberen in elke API om widgets te maken, lezen, bijwerken en verwijderen via de API.
Uw toepassing vergroten
Nu het volledige end-to-endproces werkt, gaat u verder met het bouwen van uw API:
- Meer informatie over de TypeSpec-taal om meer API's en API-laagfuncties toe te voegen in de
./main.tsp. - Voeg aanvullende emitters toe en configureer hun parameters in de
./tspconfig.yaml. - Naarmate u meer functies aan uw TypeSpec-bestanden toevoegt, kunt u deze wijzigingen ondersteunen met broncode in het serverproject.
- Ga door met het gebruik van verificatie zonder wachtwoord met Azure Identity.
De hulpbronnen opschonen
Wanneer u klaar bent met deze quickstart, kunt u de Azure-resources verwijderen:
azd down
Of verwijder de resourcegroep rechtstreeks vanuit Azure Portal.
Troubleshooting
Niet voldaan aan .NET 9-vereiste
Fout:Build error: This project requires .NET 9. You have <version> installed.
Solution:
- Controleer de .NET-versie:
dotnet --version. - Installeer .NET 9 vanuit dot.net.
HTTPS-certificaatfout
Fout:System.IO.IOException: The certificate generation failed bij het uitvoeren van dotnet dev-certs https
Solution:
- Vertrouw het bestaande certificaat:
dotnet dev-certs https --trust. - Als dat mislukt, reinigt en maakt u het opnieuw aan:
dotnet dev-certs https --clean, en dandotnet dev-certs https. - Zorg ervoor dat u de terminal als beheerder uitvoert in Windows.
@typespec/http-server-csharp emitterconflict
Fout:Emitter error: Dependency conflict bij de uitvoering tsp compile .
Solution:
Controleer of
tspconfig.yamlzowelemit:als (beide) emitters bevat.emit: - "@typespec/openapi3" - "@typespec/http-server-csharp"Cache wissen:
npm cache clean --forceen probeer het vervolgens opnieuw:tsp compile ..
Cosmos DB-verificatie mislukt
Fout:Azure.Identity.AuthenticationFailedException Of Connection string/key not set
Solution:
- Zorg ervoor dat de omgevingsvariabele is ingesteld:
echo $AZURE_COSMOS_ENDPOINT(macOS/Linux) ofecho %AZURE_COSMOS_ENDPOINT%(Windows). - Voor lokale ontwikkeling stelt u de waarde in
Program.csof een.envbestand in. - Voor productie moet u ervoor zorgen dat het container-app-geheim is geconfigureerd (zie de sectie Implementatie-infrastructuur maken).
Swagger UI toont een 404-fout
Fout: De browser toont 404 Not Found bij /swagger
Solution:
Controleer de configuratie van het Swagger-eindpunt in
Program.cs:c.SwaggerEndpoint("/openapi.yaml", "TypeSpec Generated OpenAPI Docs"); c.RoutePrefix = "swagger";Zorg ervoor dat
openapi.yamldeze zich in dewwwroot/map bevindt.Herbouwen en opnieuw opstarten:
dotnet clean && dotnet build && dotnet run.