Genomgång av kod: Serverlöst program med Functions

Azure Event Hubs
Azure Functions

Serverlösa modeller abstraherar kod från den underliggande beräkningsinfrastrukturen så att utvecklare kan fokusera på affärslogik utan omfattande installation. Serverlös kod minskar kostnaderna eftersom du endast betalar för kodkörningsresurserna och varaktigheten.

Den serverlösa händelsedrivna modellen passar situationer där en viss händelse utlöser en definierad åtgärd. Om du till exempel tar emot ett inkommande enhetsmeddelande utlöses lagring för senare användning, eller så utlöser en databasuppdatering ytterligare bearbetning.

För att hjälpa dig att utforska serverlösa Azure-tekniker i Azure har Microsoft utvecklat och testat ett serverlöst program som använder Azure Functions. Den här artikeln går igenom koden för den serverlösa Functions-lösningen och beskriver designbeslut, implementeringsinformation och några av de "gotchas" som du kan stöta på.

Utforska lösningen

Lösningen i två delar beskriver ett hypotetiskt drönarleveranssystem. Drönarna skickar flygstatus till molnet, där meddelandena sparas för senare användning. Med en webbapp kan användarna hämta meddelandena för att få den senaste statusen för enheterna.

Du kan ladda ned koden för den här lösningen från GitHub.

Den här genomgången förutsätter grundläggande kunskaper om följande tekniker:

Du behöver inte vara expert på Functions eller Event Hubs, men du bör känna till deras funktioner på hög nivå. Här följer några bra resurser för att komma igång:

Förstå scenariot

Diagram of the functional blocks

Fabrikam hanterar en flotta med drönare för en drönarleveranstjänst. Programmet består av två huvudfunktionsområden:

  • Händelseinmatning. Under flygning skickar drönare statusmeddelanden till en molnslutpunkt. Programmet matar in och bearbetar dessa meddelanden och skriver resultatet till en serverdelsdatabas (Azure Cosmos DB). Enheterna skickar meddelanden i protokollbuffertformat (protobuf). Protobuf är ett effektivt och självbeskrivande serialiseringsformat.

    Dessa meddelanden innehåller partiella uppdateringar. Vid ett fast intervall skickar varje drönare ett ”nyckelramsmeddelande” som innehåller alla statusfält. Mellan nyckelramarna innefattar statusmeddelandena endast fält som har ändrats sedan det senaste meddelandet. Det här beteendet är typiskt för många IoT-enheter som behöver spara bandbredd och ström.

  • Webbapp. En webbapp gör att användarna kan leta upp en enhet och köra frågor om enhetens senast kända status. Användare måste logga in på programmet och autentisera med Microsoft Entra-ID. Programmet tillåter endast begäranden från användare som auktoriserats att komma åt appen.

Här är en skärmbild av webbappen som visar resultatet av en fråga:

Screenshot of client app

Utforma programmet

Fabrikam har valt att använda Azure Functions för att implementera programmets affärslogik. Azure Functions är ett exempel på ”funktioner som en tjänst” (FaaS). I den här beräkningsmodellen är en funktion en kod som distribueras till molnet och körs i en värdmiljö. Värdmiljön abstraherar helt de servrar som kör koden.

Varför bör man välja en serverlös metod?

En serverlös arkitektur med Functions är ett exempel på en händelsedriven arkitektur. Funktionskoden utlöses av en händelse som är extern för funktionen – i det här fallet antingen ett meddelande från en drönare eller en HTTP-begäran från ett klientprogram. Med en funktionsapp behöver du inte skriva någon kod för utlösaren. Du skriver bara den kod som körs som svar på utlösaren. Det innebär att du kan fokusera på affärslogiken i stället för att skriva stora mängder kod som hanterar infrastrukturaspekter såsom meddelanden.

Det finns även vissa operativa fördelar med en serverlösa arkitekturer:

  • Du behöver inte hantera servrar.
  • Beräkningsresurser allokeras dynamiskt efter behov.
  • Du debiteras endast för de beräkningsresurser som används för att köra din kod.
  • Beräkningsresurser skalas på begäran baserat på trafik.

Arkitektur

Följande diagram visar programmets högnivåarkitektur:

Diagram showing the high-level architecture of the serverless Functions application.

I ett dataflöde visar pilar meddelanden som går från Enheter till Händelsehubbar och utlöser funktionsappen. Från appen visar en pil meddelanden med obeställbara bokstäver som går till en lagringskö, och en annan pil visar skrivning till Azure Cosmos DB. I ett annat dataflöde visar pilar att klientwebbappen hämtar statiska filer från bloblagringens statiska webbvärd via ett CDN. En annan pil visar klientens HTTP-begäran som går via API Management. Från API Management visar en pil funktionsappen som utlöser och läser data från Azure Cosmos DB. En annan pil visar autentisering via Microsoft Entra-ID. En användare loggar också in på Microsoft Entra-ID.

Händelseinmatning:

  1. Drönarmeddelanden matas in av Azure Event Hubs.
  2. Händelsehubbar genererar en ström av händelser som innehåller meddelandedata.
  3. Händelserna utlöser en Azure Functions-app som bearbetar dem.
  4. Resultaten lagras i Azure Cosmos DB.

Webbapp:

  1. Statiska filer betjänas av CDN från Blob Storage.
  2. En användare loggar in på webbappen med Hjälp av Microsoft Entra-ID.
  3. Azure API Management fungerar som en gateway som gör en REST API-slutpunkt tillgänglig.
  4. HTTP-begäranden från klienten utlöser en Azure Functions-app som läser från Azure Cosmos DB och returnerar resultatet.

Det här programmet baseras på två referensarkitekturer som motsvarar de två funktionella block som beskrivs ovan:

Du kan läsa de artiklarna om du vill veta mer om högnivåarkitekturen, de Azure-tjänster som används i lösningen samt överväganden gällande skalbarhet, säkerhet och tillförlitlighet.

Funktion för drönartelemetri

Vi börjar med att titta på den funktion som bearbetar drönarmeddelanden från Event Hubs. Funktionen definieras i en klass som heter RawTelemetryFunction:

namespace DroneTelemetryFunctionApp
{
    public class RawTelemetryFunction
    {
        private readonly ITelemetryProcessor telemetryProcessor;
        private readonly IStateChangeProcessor stateChangeProcessor;
        private readonly TelemetryClient telemetryClient;

        public RawTelemetryFunction(ITelemetryProcessor telemetryProcessor, IStateChangeProcessor stateChangeProcessor, TelemetryClient telemetryClient)
        {
            this.telemetryProcessor = telemetryProcessor;
            this.stateChangeProcessor = stateChangeProcessor;
            this.telemetryClient = telemetryClient;
        }
    }
    ...
}

Den här klassen har flera beroenden, som matas in i konstruktorn via beroendeinmatning:

  • ITelemetryProcessor- och IStateChangeProcessor-gränssnitten definiera två hjälpobjekt. Som vi ser utför de här objekten det mesta av arbetet.

  • TelemetryClient är en del av Application Insights SDK. Den används för att skicka anpassade programmått till Application Insights.

Senare tittar vi på hur du konfigurerar beroendeinmatning. För tillfället förutsätter vi helt enkelt att beroendena finns.

Konfigurera Event Hubs-utlösaren

Logiken i funktionen implementeras som en asynkron metod som heter RunAsync. Här är metodsignaturen:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    // implementation goes here
}

Metoden tar följande parametrar:

  • messages är en matris med händelsehubbsmeddelanden.
  • deadLetterMessages är en Azure Storage-kö som används för att lagra obeställbara meddelanden.
  • logging tillhandahåller ett loggningsgränssnitt för att skriva programloggar. De här loggarna skickas till Azure Monitor.

Attributet EventHubTrigger i parametern messages konfigurerar utlösaren. Attributets egenskaper anger händelsehubbsnamn, en anslutningssträng och en konsumentgrupp. (En konsumentgrupp är en isolerad vy över händelseströmmen för Event Hubs. Den här abstraktionen möjliggör flera användare av samma händelsehubb.)

Lägg märke till procenttecknen (%) i en del av attributegenskaperna. De visar att egenskapen anger namnet på en appinställning, och det faktiska värdet hämtas från den appinställningen vid körning. I annat fall ger egenskapen, utan procenttecken, literalvärdet.

Egenskapen Connection är ett undantag. Den här egenskapen anger alltid ett appinställningsnamn, aldrig ett literalvärde, och därför behövs inte procenttecknet. Anledningen till den här skillnaden är att en anslutningssträng är hemlig och aldrig ska checkas in i källkod.

Även om de andra två egenskaperna (händelsehubbsnamn och konsumentgrupp) inte är känsliga data som en anslutningssträng är det fortfarande bättre att placera dem i appinställningar i stället för att hårdkoda dem. På så sätt kan de uppdateras utan att appen behöver kompileras på nytt.

Mer information om hur du konfigurerar den här utlösaren finns i Azure Event Hubs-bindningar för Azure Functions.

Logik för meddelandebearbetning

Här är implementeringen av den RawTelemetryFunction.RunAsync-metod som bearbetar en batch med meddelanden:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,
    ILogger logger)
{
    telemetryClient.GetMetric("EventHubMessageBatchSize").TrackValue(messages.Length);

    foreach (var message in messages)
    {
        DeviceState deviceState = null;

        try
        {
            deviceState = telemetryProcessor.Deserialize(message.Body.Array, logger);

            try
            {
                await stateChangeProcessor.UpdateState(deviceState, logger);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error updating status document", deviceState);
                await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message, DeviceState = deviceState });
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
            await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
        }
    }
}

När funktionen anropas innehåller parametern messages en matris med meddelanden från händelsehubben. Bearbetning av meddelanden i batchar ger vanligtvis bättre prestanda än att läsa ett meddelande i taget. Du måste dock se till att funktionen är motståndskraftig och hanterar fel och undantag korrekt. Annars kan återstående meddelanden gå förlorade om funktionen kastar ett ohanterat undantag mitt i en batch. Detta beskrivs utförligare i avsnittet Felhantering.

Men om du ignorerar undantagshanteringen är bearbetningslogiken för varje meddelande enkel:

  1. Anropa ITelemetryProcessor.Deserialize för att deserialisera det meddelande som innehåller en enhetstillståndsändring.
  2. Anropa IStateChangeProcessor.UpdateState för att bearbeta tillståndsändringen.

Vi tittar närmare på de här två metoderna och börjar med metoden Deserialize.

Metoden Deserialize

Metoden TelemetryProcess.Deserialize tar en bytematris som innehåller meddelandenyttolasten. Den deserialiserar nyttolasten och returnerar ett DeviceState-objekt som representerar tillståndet för en drönare. Tillståndet kan representera en partiell uppdatering som bara innehåller delta från det senast kända tillståndet. Därför behöver metoden hantera null-fält i den deserialiserade nyttolasten.

public class TelemetryProcessor : ITelemetryProcessor
{
    private readonly ITelemetrySerializer<DroneState> serializer;

    public TelemetryProcessor(ITelemetrySerializer<DroneState> serializer)
    {
        this.serializer = serializer;
    }

    public DeviceState Deserialize(byte[] payload, ILogger log)
    {
        DroneState restored = serializer.Deserialize(payload);

        log.LogInformation("Deserialize message for device ID {DeviceId}", restored.DeviceId);

        var deviceState = new DeviceState();
        deviceState.DeviceId = restored.DeviceId;

        if (restored.Battery != null)
        {
            deviceState.Battery = restored.Battery;
        }
        if (restored.FlightMode != null)
        {
            deviceState.FlightMode = (int)restored.FlightMode;
        }
        if (restored.Position != null)
        {
            deviceState.Latitude = restored.Position.Value.Latitude;
            deviceState.Longitude = restored.Position.Value.Longitude;
            deviceState.Altitude = restored.Position.Value.Altitude;
        }
        if (restored.Health != null)
        {
            deviceState.AccelerometerOK = restored.Health.Value.AccelerometerOK;
            deviceState.GyrometerOK = restored.Health.Value.GyrometerOK;
            deviceState.MagnetometerOK = restored.Health.Value.MagnetometerOK;
        }
        return deviceState;
    }
}

Den här metoden använder ett annat hjälpgränssnitt, ITelemetrySerializer<T>, för att deserialisera det råa meddelandet. Resultatet omvandlas sedan till en POCO-modell som är lättare att arbeta med. Den här designen hjälper till att isolera bearbetningslogiken från de detaljer som rör implementering av serialisering. ITelemetrySerializer<T>-gränssnittet definieras i ett delat bibliotek som även används av enhetssimulatorn för att generera simulerade enhetshändelser och skicka dem till Event Hubs.

using System;

namespace Serverless.Serialization
{
    public interface ITelemetrySerializer<T>
    {
        T Deserialize(byte[] message);

        ArraySegment<byte> Serialize(T message);
    }
}

Metoden UpdateState

Metoden StateChangeProcessor.UpdateState tillämpar tillståndsändringarna. Det senast kända tillståndet för varje drönare lagras som ett JSON-dokument i Azure Cosmos DB. Eftersom drönarna skickar partiella uppdateringar kan programmet inte bara skriva över dokumentet när det får en uppdatering. I stället behöver det hämta det föregående tillståndet, sammanslå fälten och sedan utföra en upsert-åtgärd.

public class StateChangeProcessor : IStateChangeProcessor
{
    private IDocumentClient client;
    private readonly string cosmosDBDatabase;
    private readonly string cosmosDBCollection;

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    {
        this.client = client;
        this.cosmosDBDatabase = options.Value.COSMOSDB_DATABASE_NAME;
        this.cosmosDBCollection = options.Value.COSMOSDB_DATABASE_COL;
    }

    public async Task<ResourceResponse<Document>> UpdateState(DeviceState source, ILogger log)
    {
        log.LogInformation("Processing change message for device ID {DeviceId}", source.DeviceId);

        DeviceState target = null;

        try
        {
            var response = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(cosmosDBDatabase, cosmosDBCollection, source.DeviceId),
                                                            new RequestOptions { PartitionKey = new PartitionKey(source.DeviceId) });

            target = (DeviceState)(dynamic)response.Resource;

            // Merge properties
            target.Battery = source.Battery ?? target.Battery;
            target.FlightMode = source.FlightMode ?? target.FlightMode;
            target.Latitude = source.Latitude ?? target.Latitude;
            target.Longitude = source.Longitude ?? target.Longitude;
            target.Altitude = source.Altitude ?? target.Altitude;
            target.AccelerometerOK = source.AccelerometerOK ?? target.AccelerometerOK;
            target.GyrometerOK = source.GyrometerOK ?? target.GyrometerOK;
            target.MagnetometerOK = source.MagnetometerOK ?? target.MagnetometerOK;
        }
        catch (DocumentClientException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                target = source;
            }
        }

        var collectionLink = UriFactory.CreateDocumentCollectionUri(cosmosDBDatabase, cosmosDBCollection);
        return await client.UpsertDocumentAsync(collectionLink, target);
    }
}

Den här koden använder IDocumentClient gränssnittet för att hämta ett dokument från Azure Cosmos DB. Om dokumentet finns sammanslås de nya tillståndsvärdena i det befintliga dokumentet. Annars skapas ett nytt dokument. Båda fallen hanteras av metoden UpsertDocumentAsync.

Den här koden är optimerad för de fall där dokumentet redan finns och kan sammanslås. I det första telemetrimeddelandet från en drönare kastar metoden ReadDocumentAsync ett undantag eftersom det inte finns något dokument för den drönaren. Efter det första meddelandet blir dokumentet tillgängligt.

Observera att den här klassen använder beroendeinmatning för att mata in IDocumentClient för Azure Cosmos DB och en IOptions<T> med konfigurationsinställningar. Senare går vi igenom hur du konfigurerar beroendeinmatningen.

Kommentar

Azure Functions stöder en utdatabindning för Azure Cosmos DB. Med den här bindningen kan funktionsappen skriva dokument i Azure Cosmos DB utan kod. Dock fungerar inte utdatabindningen för just det här scenariot på grund av den anpassade upsert-logik som behövs.

Felhantering

Som tidigare nämnts bearbetar RawTelemetryFunction-funktionsappen en batch med meddelanden i en loop. Det innebär att funktionen behöver hantera eventuella undantag korrekt och fortsätta bearbeta resten av batchen. Annars kan det hända att meddelanden förloras.

Om ett undantag påträffas vid bearbetning av ett meddelande placerar funktionen meddelandet i en kö för obeställbara meddelanden:

catch (Exception ex)
{
    logger.LogError(ex, "Error deserializing message", message.SystemProperties.PartitionKey, message.SystemProperties.SequenceNumber);
    await deadLetterMessages.AddAsync(new DeadLetterMessage { Exception = ex, EventData = message });
}

Kön för obeställbara definieras med hjälp av en utdatabindning till en lagringskö:

[FunctionName("RawTelemetryFunction")]
[StorageAccount("DeadLetterStorage")]  // App setting that holds the connection string
public async Task RunAsync(
    [EventHubTrigger("%EventHubName%", Connection = "EventHubConnection", ConsumerGroup ="%EventHubConsumerGroup%")]EventData[] messages,
    [Queue("deadletterqueue")] IAsyncCollector<DeadLetterMessage> deadLetterMessages,  // output binding
    ILogger logger)

Här specificerar attributet Queue utdatabindningen, och attributet StorageAccount specificerar namnet på en appinställning som innehåller anslutningssträngen för lagringskontot.

Distributionstips: I Resource Manager-mallen som skapar lagringskontot kan du automatiskt fylla i en appinställning med anslutningssträng. Tricket är att använda funktionen listKeys .

Här är den del av mallen som skapar lagringskontot för kön:

    {
        "name": "[variables('droneTelemetryDeadLetterStorageQueueAccountName')]",
        "type": "Microsoft.Storage/storageAccounts",
        "location": "[resourceGroup().location]",
        "apiVersion": "2017-10-01",
        "sku": {
            "name": "[parameters('storageAccountType')]"
        },

Här är den del av mallen som skapar funktionsappen.


    {
        "apiVersion": "2015-08-01",
        "type": "Microsoft.Web/sites",
        "name": "[variables('droneTelemetryFunctionAppName')]",
        "location": "[resourceGroup().location]",
        "tags": {
            "displayName": "Drone Telemetry Function App"
        },
        "kind": "functionapp",
        "dependsOn": [
            "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            ...
        ],
        "properties": {
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
            "siteConfig": {
                "appSettings": [
                    {
                        "name": "DeadLetterStorage",
                        "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('droneTelemetryDeadLetterStorageQueueAccountName'), ';AccountKey=', listKeys(variables('droneTelemetryDeadLetterStorageQueueAccountId'),'2015-05-01-preview').key1)]"
                    },
                    ...

Detta definierar en appinställning med namnet DeadLetterStorage vars värde fylls i med hjälp av funktionen listKeys. Det är viktigt att göra så att funktionsappens resurs beror på lagringskontots resurs (se elementet dependsOn). Detta säkerställer att lagringskontot skapas först och att anslutningssträngen är tillgänglig.

Konfigurera beroendeinmatning

Följande kod konfigurerar beroendeinmatning för funktionen RawTelemetryFunction:

[assembly: FunctionsStartup(typeof(DroneTelemetryFunctionApp.Startup))]

namespace DroneTelemetryFunctionApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddOptions<StateChangeProcessorOptions>()
                .Configure<IConfiguration>((configSection, configuration) =>
                {
                    configuration.Bind(configSection);
                });

            builder.Services.AddTransient<ITelemetrySerializer<DroneState>, TelemetrySerializer<DroneState>>();
            builder.Services.AddTransient<ITelemetryProcessor, TelemetryProcessor>();
            builder.Services.AddTransient<IStateChangeProcessor, StateChangeProcessor>();

            builder.Services.AddSingleton<IDocumentClient>(ctx => {
                var config = ctx.GetService<IConfiguration>();
                var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
                var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
                return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
            });
        }
    }
}

Azure Functions som skrivs för .NET kan använda ASP.NET Core-ramverket för beroendeinmatning. Grundtanken är att du deklarerar en startmetod för sammansättningen. Metoden tar ett IFunctionsHostBuilder-gränssnitt som används för att deklarera beroenden för DI. Du gör detta genom att anropa metoden Add*Services-objektet. När du lägger till ett beroende anger du dess livslängd:

  • Tillfälliga objekt skapas varje gång de begärs.
  • Omfångsbaserade objekt skapas en gång per funktionskörning.
  • Singleton-objekt återanvänds mellan funktionskörningar inom livslängden för funktionsvärden.

I det här exemplet deklareras TelemetryProcessor- och StateChangeProcessor-objekten som tillfälliga. Detta är lämpligt för lätta, tillståndslösa tjänster. Klassen DocumentClient å andra sidan bör vara en singleton för bästa prestanda. Mer information finns i Prestandatips för Azure Cosmos DB och .NET.

Om du tittar på koden för RawTelemetryFunction igen ser du att det finns ett annat beroende som inte visas i DI-konfigurationskoden, nämligen klassen TelemetryClient, som används för att logga programmått. Functions-körningen registrerar automatiskt den här klassen i DI-containern, så du behöver inte registrera den explicit.

Mer information om DI i Azure Functions finns i följande artiklar:

Skicka konfigurationsinställningar i DI

Ibland måste ett objekt initieras med vissa konfigurationsvärden. I allmänhet bör de här inställningarna kommer från appinställningar eller (om det gäller hemligheter) från Azure Key Vault.

Det finns två exempel i det här programmet. DocumentClient Först tar klassen en Azure Cosmos DB-tjänstslutpunkt och nyckel. För det här objektet registrerar programmet ett lambda som kommer att anropas av DI-containern. Det här lambdat använder IConfiguration-gränssnittet för att läsa konfigurationsvärdena:

builder.Services.AddSingleton<IDocumentClient>(ctx => {
    var config = ctx.GetService<IConfiguration>();
    var cosmosDBEndpoint = config.GetValue<string>("CosmosDBEndpoint");
    var cosmosDBKey = config.GetValue<string>("CosmosDBKey");
    return new DocumentClient(new Uri(cosmosDBEndpoint), cosmosDBKey);
});

Det andra exemplet är klassen StateChangeProcessor. För det här objektet använder vi ett tillvägagångssätt som kallas alternativmönster. Så här fungerar det:

  1. Definiera en klass som kallas T och som innehåller dina konfigurationsinställningar. I det här fallet namnet på Azure Cosmos DB-databasen och samlingsnamnet.

    public class StateChangeProcessorOptions
    {
        public string COSMOSDB_DATABASE_NAME { get; set; }
        public string COSMOSDB_DATABASE_COL { get; set; }
    }
    
  2. Lägg till klassen T som en alternativklass för DI.

    builder.Services.AddOptions<StateChangeProcessorOptions>()
        .Configure<IConfiguration>((configSection, configuration) =>
        {
            configuration.Bind(configSection);
        });
    
  3. I konstruktorn för den klass som konfigureras inkluderar du en IOptions<T>-parameter.

    public StateChangeProcessor(IDocumentClient client, IOptions<StateChangeProcessorOptions> options)
    

DI-systemet fyller automatiskt i alternativklassen med konfigurationsvärden och skickar detta till konstruktorn.

Det finns flera fördelar med det här tillvägagångssättet:

  • Det frigör klassen från källan till konfigurationsvärdena.
  • Det blir enkelt att konfigurera olika konfigurationskällor, till exempel miljövariabler och JSON-konfigurationsfiler.
  • Enklare enhetstestning.
  • Använd en starkt typad alternativklass, vilket är mindre felbenäget än att bara skicka skalära värden.

Funktionen GetStatus

Den andra funktionsappen i den här lösningen implementerar ett enkelt REST-API för att hämta den senast kända statusen för en drönare. Den här funktionen definieras i en klass som heter GetStatusFunction. Här är den fullständiga koden för funktionen:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading.Tasks;

namespace DroneStatusFunctionApp
{
    public static class GetStatusFunction
    {
        public const string GetDeviceStatusRoleName = "GetStatus";

        [FunctionName("GetStatusFunction")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]HttpRequest req,
            [CosmosDB(
                databaseName: "%COSMOSDB_DATABASE_NAME%",
                collectionName: "%COSMOSDB_DATABASE_COL%",
                ConnectionStringSetting = "COSMOSDB_CONNECTION_STRING",
                Id = "{Query.deviceId}",
                PartitionKey = "{Query.deviceId}")] dynamic deviceStatus,
            ClaimsPrincipal principal,
            ILogger log)
        {
            log.LogInformation("Processing GetStatus request.");

            if (!principal.IsAuthorizedByRoles(new[] { GetDeviceStatusRoleName }, log))
            {
                return new UnauthorizedResult();
            }

            string deviceId = req.Query["deviceId"];
            if (deviceId == null)
            {
                return new BadRequestObjectResult("Missing DeviceId");
            }

            if (deviceStatus == null)
            {
                return new NotFoundResult();
            }
            else
            {
                return new OkObjectResult(deviceStatus);
            }
        }
    }
}

Den här funktionen använder en HTTP-utlösare för att bearbeta en HTTP GET-begäran. Funktionen använder en Azure Cosmos DB-indatabindning för att hämta det begärda dokumentet. En sak att tänka på är att den här bindningen körs innan auktoriseringslogiken utförs inuti funktionen. Om en obehörig användare begär ett dokument hämtar funktionsbindningen dokumentet ändå. Auktoriseringskoden returnerar då ett 401-fel så att användaren inte ser dokumentet. Huruvida det här beteendet är godtagbart kan bero på dina krav. Till exempel kan den här metoden göra det svårare att granska dataåtkomst för känsliga data.

Autentisering och auktorisering

Webbappen använder Microsoft Entra-ID för att autentisera användare. Eftersom appen är en ensidesapplikation (SPA) som körs i webbläsaren är det implicita beviljandeflödet lämpligt:

  1. Webbappen omdirigerar användaren till identitetsprovidern (i det här fallet Microsoft Entra-ID).
  2. Användaren anger sina autentiseringsuppgifter.
  3. Identitetsprovidern omdirigerar tillbaka till webbappen med en åtkomsttoken.
  4. Webbappen skickar en begäran till webb-API:et och inkluderar åtkomsttoken i auktoriseringsrubriken.

Implicit flow diagram

Ett funktionsprogram kan konfigureras för att autentisera användare helt utan kod. Mer information finns i Autentisering och auktorisering i Azure App Service.

Auktorisering å andra sidan kräver oftast viss affärslogik. Microsoft Entra-ID stöder anspråksbaserad autentisering. I den här modellen representeras en användares identitet av en uppsättning anspråk som kommer från identitetsprovidern. Ett anspråk kan vara valfri typ av information om användaren, till exempel namn eller e-postadress.

Åtkomsttoken innehåller en delmängd av användaranspråk. Bland dessa finns eventuella programroller som användaren har tilldelats till.

Parametern principal för funktionen är ett ClaimsPrincipal-objekt som innehåller anspråken från åtkomsttoken. Varje anspråk är ett nyckel/värde-par av anspråkstyp och med anspråksvärde. Programmet använder dessa för att auktorisera begäran.

Följande tilläggsmetod testar huruvida ett ClaimsPrincipal-objekt innehåller en uppsättning roller. Den returnerar false om någon av de angivna rollerna saknas. Om metoden returnerar falskt returnerar funktionen HTTP 401 (obehörig).

namespace DroneStatusFunctionApp
{
    public static class ClaimsPrincipalAuthorizationExtensions
    {
        public static bool IsAuthorizedByRoles(
            this ClaimsPrincipal principal,
            string[] roles,
            ILogger log)
        {
            var principalRoles = new HashSet<string>(principal.Claims.Where(kvp => kvp.Type == "roles").Select(kvp => kvp.Value));
            var missingRoles = roles.Where(r => !principalRoles.Contains(r)).ToArray();
            if (missingRoles.Length > 0)
            {
                log.LogWarning("The principal does not have the required {roles}", string.Join(", ", missingRoles));
                return false;
            }

            return true;
        }
    }
}

Mer information om autentisering och auktorisering i det här programmet finns i avsnittet Säkerhetsöverväganden i referensarkitekturen.

Nästa steg

När du får en känsla för hur den här referenslösningen fungerar kan du lära dig metodtips och rekommendationer för liknande lösningar.

Azure Functions är bara ett Azure-beräkningsalternativ. Hjälp med att välja beräkningsteknik finns i Välja en Azure-beräkningstjänst för ditt program.