Integrating Event Grid with GraphQL (Hot Chocolate) API

Published Feb 03 2022 05:01 AM 1,255 Views
Microsoft

Prerequisites

Why

Adding events to a microservice architecture allows other services in your application ecosystem to engage in complex workflows without losing the value of services being decoupled. You can read more about using events and how they play into a cloud-based architecture here.

Creating the Event Grid Topic

Before we can get started with any code, we should first provision the event grid topic that our events will fall under. You can read how to here. You can also create a bicep file to help with automation of the creation and the event grid topic. Here is the file bicep file where we create the event grid topic and then the API with our endpoint and access key in its appsettings.json file. You can learn more about bicep templates here.

 

 

()
param provisionParameters object
param serverFarmId string
param userAssignedIdentityId string

var teambuilderApiName = 'TeambuilderAPI${uniqueString(resourceGroup().id)}'
var teambuilderPackageUri = contains(provisionParameters, 'teambuilderPackageUri') ? provisionParameters['teambuilderPackageUri'] : 'https://github.com/microsoft/hackathon-team-builder/releases/download/v0.0.15/Teambuilder.API_0.0.15.zip'
var teambuilderEventGridTopicName = 'TeambuilderEventGrid${uniqueString(resourceGroup().id)}'

resource teambuilderEventGridTopic 'Microsoft.EventGrid/topics@2021-12-01' = {
  name: teambuilderEventGridTopicName
  location: resourceGroup().location
  identity: {
    type: 'None'
  }
  properties: {
    inputSchema: 'EventGridSchema'
    publicNetworkAccess: 'Enabled'
    disableLocalAuth: false
  }
}

resource teambuilderApi 'Microsoft.Web/sites@2021-02-01' = {
  kind: 'app'
  name: teambuilderApiName
  location: resourceGroup().location
  properties: {
    serverFarmId: serverFarmId
    keyVaultReferenceIdentity: userAssignedIdentityId    
    httpsOnly: true
    siteConfig: {
      appSettings:  [
        {
          name: 'EventGrid:AccessKey'
          value: teambuilderEventGridTopic.listKeys().key1
        }
        {
          name: 'EventGrid:Endpoint'
          value: teambuilderEventGridTopic.properties.endpoint
        }
      ]
    }
  }
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${userAssignedIdentityId}': {}
    }
  }
}

resource teamBuilderApiDeploy 'Microsoft.Web/sites/extensions@2021-02-01' = {
  parent: teambuilderApi
  name: 'MSDeploy'
  properties: {
    packageUri: teambuilderPackageUri
  }
}

output apiEndpoint string = 'https://${teambuilderApi.properties.defaultHostName}'
output eventGridTopicName string = teambuilderEventGridTopic.name
output webAppResourceId string = teambuilderApi.id

 

 

 

 

This project uses the Teambuilder Toolkit and bicep templates as part of its provisioning process. You can read more about the Teambuilder Toolkit here.

Once your topic is created, we can go ahead and jump into the code. You can find the codebase this blog is written about here.

Message Service

In this post, we are integrating event grid into an existing GraphQL API so we can start by adding the Azure.Messaging.EventGrid NuGet package. In our code, we are using version 4.8.1. Now that we have that package, let us go ahead and create a class that will be our Event Grid messaging service.

Our service will need to use the EventGridPublisherClient from NuGet package we just installed. We can now add a private read-only instantiation of that client that we will create in our constructor. The constructor for that class requires the event grid topic endpoint and an access key. So, let us add those to our constructor’s parameters. We can grab those later with dependency injection.

 

 

 

 

private readonly EventGridPublisherClient _publisherClient;

public EventGridMessageService(IOptions<EventGridMessageServiceConfiguration> config)
{
    Uri.TryCreate(config.Value.Endpoint, UriKind.RelativeOrAbsolute, out var uri);

    var accessKey = new Azure.AzureKeyCredential(config.Value.AccessKey);

    _publisherClient = new EventGridPublisherClient(uri, accessKey);
}

 

 

 

 

Now that we have our client, let us create a simple method to expose sending messages. We can start by just creating a method that just wraps the EventGridPublisherClient’s SendEventAsync method. This method takes in a subject, event type, a data version, and data. The data version will stay constant. The data will be any object that you plan to send, in our case we will be sending the entity that is being mutated. The event type is a string you can use to filter your Event Grid Subscriptions to the event grid topic against. The subject and data can be whatever you want.

 

 

 

 

public async Task SendAsync(string subject, string eventType, string dataVersion, object data)
{
    var message = new EventGridEvent(subject, eventType, dataVersion, data);   

    await _publisherClient.SendEventAsync(message);
}

 

 

 

 

When working with events, it is helpful to have a pattern you want your events to follow so that your subscriptions can know better what to expect. For our pattern, our event type will be a string with the type of the object, we are mutating at the beginning of the string, followed by a period, and followed by the type of mutation. Our subject will describe in plain text what is happening, and our data will be the mutated object.

Since we have this pattern, we can program against it to save ourselves some time using a little string interpolation, an Enum of mutation types, and the typeof operator in dotnet.
We will also make an interface for our service.

Here is our final interface:

 

 

 

 

public interface IMessageService
{
    Task SendAsync<T>(T entity, MutationType mutationType);
}

public enum MutationType
{
    Create,
    Update,
    Delete
}

 

 

 

 

…and our final service class implementing that interface.

 

 

 

 

namespace TeamBuilder.API.Services
{
    public class EventGridMessageService : IMessageService
    {
        private const string DATA_VERSION = "1.0";
        private readonly EventGridPublisherClient _publisherClient;

        public EventGridMessageService(IOptions<EventGridMessageServiceConfiguration> config)
        {
            Uri.TryCreate(config.Value.Endpoint, UriKind.RelativeOrAbsolute, out var uri);

            var accessKey = new Azure.AzureKeyCredential(config.Value.AccessKey);

            _publisherClient = new EventGridPublisherClient(uri, accessKey);
        }

        public async Task SendAsync<T>(T entity, MutationType mutationType)
        {
            var subject = $"{typeof(T).Name} {mutationType.ToString().ToLower()}d";

            var messsage = new EventGridEvent(subject, $"{typeof(T).Name}.{mutationType}", DATA_VERSION, entity);

            await _publisherClient.SendEventAsync(messsage);
        }
    }

    public class EventGridMessageServiceConfiguration
    {
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
        public string Endpoint { get; set; }
        public string AccessKey { get; set; }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
    }
}

 

 

 

 

(Note that we have created a class for receiving the access key and endpoint that we will require for our constructor.)


Dependency Injection

With our service class and interface written, we can turn our focus on how they will be called and instantiated with dotnet dependency injection. In a standard REST API, we would first add our service to the services in our Startup file and then request it in our controller constructors who need to use that class. With HotChocolate, it is not too different. We will still start by adding our service to services in Startup. We will do this prior to adding our GraphQL server. Be sure to grab your configuration from appsettings.json.

 

 

 

services.Configure<EventGridMessageServiceConfiguration>(Configuration.GetSection("EventGrid"));
services.AddSingleton<IMessageService, EventGridMessageService>();

services
    .AddGraphQLServer()
    .AddQueryType<Query>()

 

 

 

Once our service is available through dependency injection, we can add it to our mutation methods which consume services much the same way that standard REST controllers do. Since our service is a singleton, we will just label it using the service attribute in our method’s constructor.

Using the Message Service

In our mutation method, we now have access to our service and can call the service’s method to send an event after our mutation. Our pattern is the same for all our mutations, complete the mutation and immediately fire off an event under our topic using our method. It is worth noting that for creation, we send the object being created.

 

 

 

[UseTeamBuilderDbContext]
public async Task<AddChallengePayload> AddChallengeAsync(
    AddChallengeInput input,
    [ScopedService] TeamBuilderDbContext context,
    [Service] IMessageService messageService)
{
    var challenge = new ChallengeArea
    {
        Name = input.Name,
        Prefix = input.Prefix,
        Description = input.Description
    };

    context.Challenges.Add(challenge);
    await context.SaveChangesAsync();

    await messageService.SendAsync(challenge, MutationType.Create);

    return new AddChallengePayload(challenge);
}

 

 

 

For updates, we send the updated object.

 

 

 

[UseTeamBuilderDbContext]
public async Task<EditChallengePayload> EditChallengeAsync(
    int id,
    EditChallengeInput input,
    [ScopedService] TeamBuilderDbContext context,
    [Service] IMessageService messageService)
{
    var existingItem = await context.Challenges.FindAsync(id);
    if (existingItem == null)
    {
        return new EditChallengePayload(false, "Item not found.");
    }

    existingItem.Name = string.IsNullOrEmpty(input.Name) ? existingItem.Name : input.Name;
    existingItem.Prefix = string.IsNullOrEmpty(input.Prefix) ? existingItem.Prefix : input.Prefix;
    existingItem.Description = string.IsNullOrEmpty(input.Description) ? existingItem.Description : input.Description;
    context.Entry(existingItem).State = EntityState.Modified;
    await context.SaveChangesAsync();

    await messageService.SendAsync(existingItem, MutationType.Update);

    return new EditChallengePayload(existingItem);
}

 

 

 

And for deletes, we send the object that has been deleted.

 

 

 

[UseTeamBuilderDbContext]
public async Task<DeleteChallengePayload> DeleteChallengeAsync(
        int id,
        [ScopedService] TeamBuilderDbContext context,
        [Service] IMessageService messageService)
{
    var existingItem = await context.Challenges.FindAsync(id);
    if (existingItem == null)
    {
        return new DeleteChallengePayload(false, "Item not found.");
    }
    context.Challenges.Remove(existingItem);
    await context.SaveChangesAsync();

    await messageService.SendAsync(existingItem, MutationType.Delete);

    return new DeleteChallengePayload(true, "Deleted");
}

 

 

 

 

This allows our subscribers to take advantage of the data intelligently. If they need to display information about the recently deleted object, they have the entity in their message since it will no longer be available to them in our data store. Our goal for our messages is to send the most useful information without bloating our message or sending code too much.

Conclusion

Adding events with event grid to your API is simple and fast and a terrific way to add extensibility to your API in Azure. You can read the events posted in several ways. A straightforward way to do so immediately is to just add a storage queue and hook it up to an event subscription and read the queue using Azure Storage Explorer. In part 2, I talk about using an Azure Function to trigger off these events.

%3CLINGO-SUB%20id%3D%22lingo-sub-3107245%22%20slang%3D%22en-US%22%3EIntegrating%20Event%20Grid%20with%20GraphQL%20(Hot%20Chocolate)%20API%3C%2FLINGO-SUB%3E%3CLINGO-BODY%20id%3D%22lingo-body-3107245%22%20slang%3D%22en-US%22%3E%3CH1%20id%3D%22toc-hId--2139264371%22%20id%3D%22toc-hId--2138544610%22%3EPrerequisites%3C%2FH1%3E%0A%3CUL%3E%0A%3CLI%3EBase%20familiarity%20with%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Fasync%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Easync%20programming%20in%20dotnet%3C%2FA%3E%3C%2FLI%3E%0A%3CLI%3EA%20small%20%3CA%20href%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Ft5%2Fblogs%2Fblogworkflowpage%2Fblog-id%2FHealthcareAndLifeSciencesBlog%2Farticle-id%2F1186%22%20target%3D%22_blank%22%3EGraphQL%20API%20using%20HotChocolate%3C%2FA%3E%3C%2FLI%3E%0A%3CLI%3EAn%20Azure%20subscription%3C%2FLI%3E%0A%3C%2FUL%3E%0A%3CH1%20id%3D%22toc-hId-348248462%22%20id%3D%22toc-hId-348968223%22%3EWhy%3C%2FH1%3E%0A%3CP%3EAdding%20events%20to%20a%20microservice%20architecture%20allows%20other%20services%20in%20your%20application%20ecosystem%20can%20engage%20in%20complex%20workflows%20without%20losing%20the%20value%20of%20services%20being%20decoupled.%20You%20can%20read%20more%20about%20using%20events%20and%20how%20they%20play%20into%20a%20cloud-based%20architecture%20%3CA%20href%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Ft5%2Fazure-developer-community-blog%2Fjourney-towards-cloud-architecture%2Fba-p%2F2780579%22%20target%3D%22_blank%22%3Ehere%3C%2FA%3E.%3C%2FP%3E%0A%3CH1%20id%3D%22toc-hId--1459206001%22%20id%3D%22toc-hId--1458486240%22%3ECreating%20the%20Event%20Grid%20Topic%3C%2FH1%3E%0A%3CP%3EBefore%20we%20can%20get%20started%20with%20any%20code%2C%20we%20should%20first%20provision%20the%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fevent-grid%2Fconcepts%23topics%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Eevent%20grid%20topic%3C%2FA%3E%20that%20our%20events%20will%20fall%20under.%20You%20can%20read%20how%20to%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fevent-grid%2Fcreate-view-manage-system-topics%23%3A~%3Atext%3D%2520Create%2520an%2520event%2520subscription%2520%25201%2520Follow%2Cpopulated.%2520Enter%2520a%2520name%252C%2520select%2520an...%2520See%2520More.%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Ehere%3C%2FA%3E.%20You%20can%20also%20create%20a%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fazure-resource-manager%2Fbicep%2Foverview%3Ftabs%3Dbicep%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Ebicep%3C%2FA%3E%20file%20to%20help%20with%20automation%20of%20the%20creation%20and%20the%20event%20grid%20topic.%20Here%20is%20the%20file%20bicep%20file%20where%20we%20create%20the%20event%20grid%20topic%20and%20then%20the%20API%20with%20our%20endpoint%20and%20access%20key%20in%20its%20appsettings.json%20file.%20You%20can%20learn%20more%20about%20bicep%20templates%20%3CA%20href%3D%22https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dc5LaBhDN2Vk%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noreferrer%22%3Ehere%3C%2FA%3E.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-javascript%22%3E%3CCODE%3E()%0Aparam%20provisionParameters%20object%0Aparam%20serverFarmId%20string%0Aparam%20userAssignedIdentityId%20string%0A%0Avar%20teambuilderApiName%20%3D%20'TeambuilderAPI%24%7BuniqueString(resourceGroup().id)%7D'%0Avar%20teambuilderPackageUri%20%3D%20contains(provisionParameters%2C%20'teambuilderPackageUri')%20%3F%20provisionParameters%5B'teambuilderPackageUri'%5D%20%3A%20'https%3A%2F%2Fgithub.com%2Fmicrosoft%2Fhackathon-team-builder%2Freleases%2Fdownload%2Fv0.0.15%2FTeambuilder.API_0.0.15.zip'%0Avar%20teambuilderEventGridTopicName%20%3D%20'TeambuilderEventGrid%24%7BuniqueString(resourceGroup().id)%7D'%0A%0Aresource%20teambuilderEventGridTopic%20'Microsoft.EventGrid%2Ftopics%402021-12-01'%20%3D%20%7B%0A%20%20name%3A%20teambuilderEventGridTopicName%0A%20%20location%3A%20resourceGroup().location%0A%20%20identity%3A%20%7B%0A%20%20%20%20type%3A%20'None'%0A%20%20%7D%0A%20%20properties%3A%20%7B%0A%20%20%20%20inputSchema%3A%20'EventGridSchema'%0A%20%20%20%20publicNetworkAccess%3A%20'Enabled'%0A%20%20%20%20disableLocalAuth%3A%20false%0A%20%20%7D%0A%7D%0A%0Aresource%20teambuilderApi%20'Microsoft.Web%2Fsites%402021-02-01'%20%3D%20%7B%0A%20%20kind%3A%20'app'%0A%20%20name%3A%20teambuilderApiName%0A%20%20location%3A%20resourceGroup().location%0A%20%20properties%3A%20%7B%0A%20%20%20%20serverFarmId%3A%20serverFarmId%0A%20%20%20%20keyVaultReferenceIdentity%3A%20userAssignedIdentityId%20%20%20%20%0A%20%20%20%20httpsOnly%3A%20true%0A%20%20%20%20siteConfig%3A%20%7B%0A%20%20%20%20%20%20appSettings%3A%20%20%5B%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20name%3A%20'EventGrid%3AAccessKey'%0A%20%20%20%20%20%20%20%20%20%20value%3A%20teambuilderEventGridTopic.listKeys().key1%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20name%3A%20'EventGrid%3AEndpoint'%0A%20%20%20%20%20%20%20%20%20%20value%3A%20teambuilderEventGridTopic.properties.endpoint%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%5D%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20identity%3A%20%7B%0A%20%20%20%20type%3A%20'UserAssigned'%0A%20%20%20%20userAssignedIdentities%3A%20%7B%0A%20%20%20%20%20%20'%24%7BuserAssignedIdentityId%7D'%3A%20%7B%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0Aresource%20teamBuilderApiDeploy%20'Microsoft.Web%2Fsites%2Fextensions%402021-02-01'%20%3D%20%7B%0A%20%20parent%3A%20teambuilderApi%0A%20%20name%3A%20'MSDeploy'%0A%20%20properties%3A%20%7B%0A%20%20%20%20packageUri%3A%20teambuilderPackageUri%0A%20%20%7D%0A%7D%0A%0Aoutput%20apiEndpoint%20string%20%3D%20'https%3A%2F%2F%24%7BteambuilderApi.properties.defaultHostName%7D'%0Aoutput%20eventGridTopicName%20string%20%3D%20teambuilderEventGridTopic.name%0Aoutput%20webAppResourceId%20string%20%3D%20teambuilderApi.id%0A%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EThis%20project%20uses%20the%20Teambuilder%20Toolkit%20and%20bicep%20templates%20as%20part%20of%20its%20provisioning%20process.%20You%20can%20read%20more%20about%20the%20Teambuilder%20Toolkit%20%3CA%20href%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Ft5%2Fhealthcare-and-life-sciences%2Fjumpstart-your-teams-app-development-with-teams-toolkit%2Fba-p%2F3089571%22%20target%3D%22_blank%22%3Ehere%3C%2FA%3E.%3C%2FP%3E%0A%3CP%3EOnce%20your%20topic%20is%20created%2C%20we%20can%20go%20ahead%20and%20jump%20into%20the%20code.%20You%20can%20find%20the%20codebase%20this%20blog%20is%20written%20about%20%3CA%20href%3D%22https%3A%2F%2Fgithub.com%2Fmicrosoft%2Fhackathon-team-builder%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Ehere%3C%2FA%3E.%3C%2FP%3E%0A%3CH1%20id%3D%22toc-hId-1028306832%22%20id%3D%22toc-hId-1029026593%22%3EMessage%20Service%3C%2FH1%3E%0A%3CP%3EIn%20this%20post%2C%20we%20are%20integrating%20event%20grid%20into%20an%20existing%20GraphQL%20API%20so%20we%20can%20start%20by%20adding%20the%20%3CA%20href%3D%22https%3A%2F%2Fwww.nuget.org%2Fpackages%2FAzure.Messaging.EventGrid%2F%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noreferrer%22%3EAzure.Messaging.EventGrid%3C%2FA%3E%20NuGet%20package.%20In%20our%20code%2C%20we%20are%20using%20version%204.8.1.%20Now%20that%20we%20have%20that%20package%2C%20let%20us%20go%20ahead%20and%20create%20a%20class%20that%20will%20be%20our%20Event%20Grid%20messaging%20service.%3C%2FP%3E%0A%3CP%3EOur%20service%20will%20need%20to%20use%20the%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fazure.messaging.eventgrid.eventgridpublisherclient%3Fview%3Dazure-dotnet%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3EEventGridPublisherClient%3C%2FA%3E%20from%20NuGet%20package%20we%20just%20installed.%20We%20can%20now%20add%20a%20private%20read-only%20instantiation%20of%20that%20client%20that%20we%20will%20create%20in%20our%20constructor.%20The%20constructor%20for%20that%20class%20requires%20the%20event%20grid%20topic%20endpoint%20and%20an%20access%20key.%20So%2C%20let%20us%20add%20those%20to%20our%20constructor%E2%80%99s%20parameters.%20We%20can%20grab%20those%20later%20with%20dependency%20injection.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3Eprivate%20readonly%20EventGridPublisherClient%20_publisherClient%3B%0A%0Apublic%20EventGridMessageService(IOptions%3CEVENTGRIDMESSAGESERVICECONFIGURATION%3E%20config)%0A%7B%0A%20%20%20%20Uri.TryCreate(config.Value.Endpoint%2C%20UriKind.RelativeOrAbsolute%2C%20out%20var%20uri)%3B%0A%0A%20%20%20%20var%20accessKey%20%3D%20new%20Azure.AzureKeyCredential(config.Value.AccessKey)%3B%0A%0A%20%20%20%20_publisherClient%20%3D%20new%20EventGridPublisherClient(uri%2C%20accessKey)%3B%0A%7D%3C%2FEVENTGRIDMESSAGESERVICECONFIGURATION%3E%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3ENow%20that%20we%20have%20our%20client%2C%20let%20us%20create%20a%20simple%20method%20to%20expose%20sending%20messages.%20We%20can%20start%20by%20just%20creating%20a%20method%20that%20just%20wraps%20the%20EventGridPublisherClient%E2%80%99s%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fazure.messaging.eventgrid.eventgridpublisherclient.sendeventasync%3Fview%3Dazure-dotnet%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3ESendEventAsync%3C%2FA%3E%20method.%20This%20method%20takes%20in%20a%20subject%2C%20event%20type%2C%20a%20data%20version%2C%20and%20data.%20The%20data%20version%20will%20stay%20constant.%20The%20data%20will%20be%20any%20object%20that%20you%20plan%20to%20send%2C%20in%20our%20case%20we%20will%20be%20sending%20the%20entity%20that%20is%20being%20mutated.%20The%20event%20type%20is%20a%20string%20you%20can%20use%20to%20filter%20your%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fevent-grid%2Fconcepts%23event-subscriptions%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3EEvent%20Grid%20Subscriptions%3C%2FA%3E%20to%20the%20event%20grid%20topic%20against.%20The%20subject%20and%20data%20can%20be%20whatever%20you%20want.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3Epublic%20async%20Task%20SendAsync(string%20subject%2C%20string%20eventType%2C%20string%20dataVersion%2C%20object%20data)%0A%7B%0A%20%20%20%20var%20message%20%3D%20new%20EventGridEvent(subject%2C%20eventType%2C%20dataVersion%2C%20data)%3B%20%20%20%0A%0A%20%20%20%20await%20_publisherClient.SendEventAsync(message)%3B%0A%7D%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EWhen%20working%20with%20events%2C%20it%20is%20helpful%20to%20have%20a%20pattern%20you%20want%20your%20events%20to%20follow%20so%20that%20your%20subscriptions%20can%20know%20better%20what%20to%20expect.%20For%20our%20pattern%2C%20our%20event%20type%20will%20be%20a%20string%20with%20the%20type%20of%20the%20object%2C%20we%20are%20%3CA%20href%3D%22https%3A%2F%2Fgraphql.org%2Flearn%2Fqueries%2F%23mutations%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noreferrer%22%3Emutating%3C%2FA%3E%20at%20the%20beginning%20of%20the%20string%2C%20followed%20by%20a%20period%2C%20and%20followed%20by%20the%20type%20of%20mutation.%20Our%20subject%20will%20describe%20in%20plain%20text%20what%20is%20happening%2C%20and%20our%20data%20will%20be%20the%20mutated%20object.%3C%2FP%3E%0A%3CP%3ESince%20we%20have%20this%20pattern%2C%20we%20can%20program%20against%20it%20to%20save%20ourselves%20some%20time%20using%20a%20little%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Flanguage-reference%2Ftokens%2Finterpolated%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Estring%20interpolation%3C%2FA%3E%2C%20an%20Enum%20of%20mutation%20types%2C%20and%20the%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Flanguage-reference%2Foperators%2Ftype-testing-and-cast%23typeof-operator%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Etypeof%3C%2FA%3E%20operator%20in%20dotnet.%20%3CBR%20%2F%3EWe%20will%20also%20make%20an%20interface%20for%20our%20service.%3C%2FP%3E%0A%3CP%3EHere%20is%20our%20final%20interface%3A%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3Epublic%20interface%20IMessageService%0A%7B%0A%20%20%20%20Task%20SendAsync%3CT%3E(T%20entity%2C%20MutationType%20mutationType)%3B%0A%7D%0A%0Apublic%20enum%20MutationType%0A%7B%0A%20%20%20%20Create%2C%0A%20%20%20%20Update%2C%0A%20%20%20%20Delete%0A%7D%3C%2FT%3E%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%E2%80%A6and%20our%20final%20service%20class%20implementing%20that%20interface.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3Enamespace%20TeamBuilder.API.Services%0A%7B%0A%20%20%20%20public%20class%20EventGridMessageService%20%3A%20IMessageService%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20private%20const%20string%20DATA_VERSION%20%3D%20%221.0%22%3B%0A%20%20%20%20%20%20%20%20private%20readonly%20EventGridPublisherClient%20_publisherClient%3B%0A%0A%20%20%20%20%20%20%20%20public%20EventGridMessageService(IOptions%3CEVENTGRIDMESSAGESERVICECONFIGURATION%3E%20config)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20Uri.TryCreate(config.Value.Endpoint%2C%20UriKind.RelativeOrAbsolute%2C%20out%20var%20uri)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20var%20accessKey%20%3D%20new%20Azure.AzureKeyCredential(config.Value.AccessKey)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20_publisherClient%20%3D%20new%20EventGridPublisherClient(uri%2C%20accessKey)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20public%20async%20Task%20SendAsync%3CT%3E(T%20entity%2C%20MutationType%20mutationType)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20var%20subject%20%3D%20%24%22%7Btypeof(T).Name%7D%20%7BmutationType.ToString().ToLower()%7Dd%22%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20var%20messsage%20%3D%20new%20EventGridEvent(subject%2C%20%24%22%7Btypeof(T).Name%7D.%7BmutationType%7D%22%2C%20DATA_VERSION%2C%20entity)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20_publisherClient.SendEventAsync(messsage)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20public%20class%20EventGridMessageServiceConfiguration%0A%20%20%20%20%7B%0A%23pragma%20warning%20disable%20CS8618%20%2F%2F%20Non-nullable%20field%20must%20contain%20a%20non-null%20value%20when%20exiting%20constructor.%20Consider%20declaring%20as%20nullable.%0A%20%20%20%20%20%20%20%20public%20string%20Endpoint%20%7B%20get%3B%20set%3B%20%7D%0A%20%20%20%20%20%20%20%20public%20string%20AccessKey%20%7B%20get%3B%20set%3B%20%7D%0A%23pragma%20warning%20restore%20CS8618%20%2F%2F%20Non-nullable%20field%20must%20contain%20a%20non-null%20value%20when%20exiting%20constructor.%20Consider%20declaring%20as%20nullable.%0A%20%20%20%20%7D%0A%7D%3C%2FT%3E%3C%2FEVENTGRIDMESSAGESERVICECONFIGURATION%3E%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%3CEM%3E(Note%20that%20we%20have%20created%20a%20class%20for%20receiving%20the%20access%20key%20and%20endpoint%20that%20we%20will%20require%20for%20our%20constructor.)%3C%2FEM%3E%3C%2FP%3E%0A%3CP%3E%3CBR%20%2F%3E%3CSTRONG%3EDependency%20Injection%3C%2FSTRONG%3E%3C%2FP%3E%0A%3CP%3EWith%20our%20service%20class%20and%20interface%20written%2C%20we%20can%20turn%20our%20focus%20on%20how%20they%20will%20be%20called%20and%20instantiated%20with%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcore%2Fextensions%2Fdependency-injection%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Edotnet%20dependency%20injection%3C%2FA%3E.%20In%20a%20standard%20REST%20API%2C%20we%20would%20first%20add%20our%20service%20to%20the%20%3CEM%3Eservices%20%3C%2FEM%3Ein%20our%20Startup%20file%20and%20then%20request%20it%20in%20our%20controller%20constructors%20who%20need%20to%20use%20that%20class.%20With%20HotChocolate%2C%20it%20is%20not%20too%20different.%20We%20will%20still%20start%20by%20adding%20our%20service%20to%20%3CEM%3Eservices%20%3C%2FEM%3Ein%20Startup.%20We%20will%20do%20this%20prior%20to%20adding%20our%20GraphQL%20server.%20Be%20sure%20to%20grab%20your%20configuration%20from%20appsettings.json.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3Eservices.Configure%3CEVENTGRIDMESSAGESERVICECONFIGURATION%3E(Configuration.GetSection(%22EventGrid%22))%3B%0Aservices.AddSingleton%3CIMESSAGESERVICE%3E()%3B%0A%0Aservices%0A%20%20%20%20.AddGraphQLServer()%0A%20%20%20%20.AddQueryType%3CQUERY%3E()%3C%2FQUERY%3E%3C%2FIMESSAGESERVICE%3E%3C%2FEVENTGRIDMESSAGESERVICECONFIGURATION%3E%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EOnce%20our%20service%20is%20available%20through%20dependency%20injection%2C%20we%20can%20add%20it%20to%20our%20mutation%20methods%20which%20%3CA%20href%3D%22https%3A%2F%2Fchillicream.com%2Fdocs%2Fhotchocolate%2Fapi-reference%2Fdependency-injection%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noreferrer%22%3Econsume%20services%3C%2FA%3E%20much%20the%20same%20way%20that%20standard%20REST%20controllers%20do.%20Since%20our%20service%20is%20a%20singleton%2C%20we%20will%20just%20label%20it%20using%20the%20service%20attribute%20in%20our%20method%E2%80%99s%20constructor.%3C%2FP%3E%0A%3CH1%20id%3D%22toc-hId--779147631%22%20id%3D%22toc-hId--778427870%22%3EUsing%20the%20Message%20Service%3C%2FH1%3E%0A%3CP%3EIn%20our%20mutation%20method%2C%20we%20now%20have%20access%20to%20our%20service%20and%20can%20call%20the%20service%E2%80%99s%20method%20to%20send%20an%20event%20after%20our%20mutation.%20Our%20pattern%20is%20the%20same%20for%20all%20our%20mutations%2C%20complete%20the%20mutation%20and%20immediately%20fire%20off%20an%20event%20under%20our%20topic%20using%20our%20method.%20It%20is%20worth%20noting%20that%20for%20creation%2C%20we%20send%20the%20object%20being%20created.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3E%5BUseTeamBuilderDbContext%5D%0Apublic%20async%20Task%3CADDCHALLENGEPAYLOAD%3E%20AddChallengeAsync(%0A%20%20%20%20AddChallengeInput%20input%2C%0A%20%20%20%20%5BScopedService%5D%20TeamBuilderDbContext%20context%2C%0A%20%20%20%20%5BService%5D%20IMessageService%20messageService)%0A%7B%0A%20%20%20%20var%20challenge%20%3D%20new%20ChallengeArea%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20Name%20%3D%20input.Name%2C%0A%20%20%20%20%20%20%20%20Prefix%20%3D%20input.Prefix%2C%0A%20%20%20%20%20%20%20%20Description%20%3D%20input.Description%0A%20%20%20%20%7D%3B%0A%0A%20%20%20%20context.Challenges.Add(challenge)%3B%0A%20%20%20%20await%20context.SaveChangesAsync()%3B%0A%0A%20%20%20%20await%20messageService.SendAsync(challenge%2C%20MutationType.Create)%3B%0A%0A%20%20%20%20return%20new%20AddChallengePayload(challenge)%3B%0A%7D%3C%2FADDCHALLENGEPAYLOAD%3E%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EFor%20updates%2C%20we%20send%20the%20updated%20object.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3E%5BUseTeamBuilderDbContext%5D%0Apublic%20async%20Task%3CEDITCHALLENGEPAYLOAD%3E%20EditChallengeAsync(%0A%20%20%20%20int%20id%2C%0A%20%20%20%20EditChallengeInput%20input%2C%0A%20%20%20%20%5BScopedService%5D%20TeamBuilderDbContext%20context%2C%0A%20%20%20%20%5BService%5D%20IMessageService%20messageService)%0A%7B%0A%20%20%20%20var%20existingItem%20%3D%20await%20context.Challenges.FindAsync(id)%3B%0A%20%20%20%20if%20(existingItem%20%3D%3D%20null)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%20new%20EditChallengePayload(false%2C%20%22Item%20not%20found.%22)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20existingItem.Name%20%3D%20string.IsNullOrEmpty(input.Name)%20%3F%20existingItem.Name%20%3A%20input.Name%3B%0A%20%20%20%20existingItem.Prefix%20%3D%20string.IsNullOrEmpty(input.Prefix)%20%3F%20existingItem.Prefix%20%3A%20input.Prefix%3B%0A%20%20%20%20existingItem.Description%20%3D%20string.IsNullOrEmpty(input.Description)%20%3F%20existingItem.Description%20%3A%20input.Description%3B%0A%20%20%20%20context.Entry(existingItem).State%20%3D%20EntityState.Modified%3B%0A%20%20%20%20await%20context.SaveChangesAsync()%3B%0A%0A%20%20%20%20await%20messageService.SendAsync(existingItem%2C%20MutationType.Update)%3B%0A%0A%20%20%20%20return%20new%20EditChallengePayload(existingItem)%3B%0A%7D%3C%2FEDITCHALLENGEPAYLOAD%3E%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EAnd%20for%20deletes%2C%20we%20send%20the%20object%20that%20has%20been%20deleted.%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CPRE%20class%3D%22lia-code-sample%20language-csharp%22%3E%3CCODE%3E%5BUseTeamBuilderDbContext%5D%0Apublic%20async%20Task%3CDELETECHALLENGEPAYLOAD%3E%20DeleteChallengeAsync(%0A%20%20%20%20%20%20%20%20int%20id%2C%0A%20%20%20%20%20%20%20%20%5BScopedService%5D%20TeamBuilderDbContext%20context%2C%0A%20%20%20%20%20%20%20%20%5BService%5D%20IMessageService%20messageService)%0A%7B%0A%20%20%20%20var%20existingItem%20%3D%20await%20context.Challenges.FindAsync(id)%3B%0A%20%20%20%20if%20(existingItem%20%3D%3D%20null)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20return%20new%20DeleteChallengePayload(false%2C%20%22Item%20not%20found.%22)%3B%0A%20%20%20%20%7D%0A%20%20%20%20context.Challenges.Remove(existingItem)%3B%0A%20%20%20%20await%20context.SaveChangesAsync()%3B%0A%0A%20%20%20%20await%20messageService.SendAsync(existingItem%2C%20MutationType.Delete)%3B%0A%0A%20%20%20%20return%20new%20DeleteChallengePayload(true%2C%20%22Deleted%22)%3B%0A%7D%3C%2FDELETECHALLENGEPAYLOAD%3E%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EThis%20allows%20our%20subscribers%20to%20take%20advantage%20of%20the%20data%20intelligently.%20If%20they%20need%20to%20display%20information%20about%20the%20recently%20deleted%20object%2C%20they%20have%20the%20entity%20in%20their%20message%20since%20it%20will%20no%20longer%20be%20available%20to%20them%20in%20our%20data%20store.%20Our%20goal%20for%20our%20messages%20is%20to%20send%20the%20most%20useful%20information%20without%20bloating%20our%20message%20or%20sending%20code%20too%20much.%3C%2FP%3E%0A%3CH1%20id%3D%22toc-hId-1708365202%22%20id%3D%22toc-hId-1709084963%22%3EConclusion%3C%2FH1%3E%0A%3CP%3EAdding%20events%20with%20event%20grid%20to%20your%20API%20is%20simple%20and%20fast%20and%20a%20terrific%20way%20to%20add%20extensibility%20to%20your%20API%20in%20Azure.%20You%20can%20read%20the%20events%20posted%20in%20several%20ways.%20A%20straightforward%20way%20to%20do%20so%20immediately%20is%20to%20just%20add%20a%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fstorage%2Fqueues%2Fstorage-queues-introduction%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Estorage%20queue%3C%2FA%3E%20and%20hook%20it%20up%20to%20an%20%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fevent-grid%2Fconcepts%23event-subscriptions%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Eevent%20subscription%3C%2FA%3E%20and%20read%20the%20queue%20using%20%3CA%20href%3D%22https%3A%2F%2Fazure.microsoft.com%2Fen-us%2Ffeatures%2Fstorage-explorer%2F%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3EAzure%20Storage%20Explorer%3C%2FA%3E.%20I%20hope%20to%20write%20another%20post%20on%20creating%20clients%20to%20read%20events.%3C%2FP%3E%3C%2FLINGO-BODY%3E%3CLINGO-TEASER%20id%3D%22lingo-teaser-3107245%22%20slang%3D%22en-US%22%3E%3CP%3E%3CSPAN%20class%3D%22lia-inline-image-display-wrapper%20lia-image-align-center%22%20image-alt%3D%22mjschanne_9-1643830258116.png%22%20style%3D%22width%3A%20999px%3B%22%3E%3CIMG%20src%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Ft5%2Fimage%2Fserverpage%2Fimage-id%2F344773i8F1FF960471CEB6C%2Fimage-size%2Flarge%3Fv%3Dv2%26amp%3Bpx%3D999%22%20role%3D%22button%22%20title%3D%22mjschanne_9-1643830258116.png%22%20alt%3D%22mjschanne_9-1643830258116.png%22%20%2F%3E%3C%2FSPAN%3E%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%3CSPAN%20class%3D%22TextRun%20SCXW217363184%20BCX8%22%20data-contrast%3D%22auto%22%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%3EYou%20can%20extend%20functionality%20of%20any%20%3C%2FSPAN%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%3EAPI%3C%2FSPAN%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%3E%20through%20use%20of%20a%20decentralized%20architecture%20and%20events%20through%20event%20grid.%20This%20article%20will%20cover%20adding%20events%20with%20event%20grid%20to%20a%20%3C%2FSPAN%3E%3C%2FSPAN%3E%3CA%20class%3D%22Hyperlink%20SCXW217363184%20BCX8%22%20href%3D%22https%3A%2F%2Fgraphql.org%2F%22%20target%3D%22_blank%22%20rel%3D%22noreferrer%20noopener%20nofollow%22%3E%3CSPAN%20class%3D%22TextRun%20Underlined%20SCXW217363184%20BCX8%22%20data-contrast%3D%22none%22%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%20data-ccp-charstyle%3D%22Hyperlink%22%3EGraphQL%20API%3C%2FSPAN%3E%3C%2FSPAN%3E%3C%2FA%3E%3CSPAN%20class%3D%22TextRun%20SCXW217363184%20BCX8%22%20data-contrast%3D%22auto%22%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%3E%20(%3C%2FSPAN%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%3Eusing%3C%2FSPAN%3E%20%3C%2FSPAN%3E%3CA%20class%3D%22Hyperlink%20SCXW217363184%20BCX8%22%20href%3D%22https%3A%2F%2Fchillicream.com%2Fdocs%2Fhotchocolate%22%20target%3D%22_blank%22%20rel%3D%22noreferrer%20noopener%20nofollow%22%3E%3CSPAN%20class%3D%22TextRun%20Underlined%20SCXW217363184%20BCX8%22%20data-contrast%3D%22none%22%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%20data-ccp-charstyle%3D%22Hyperlink%22%3EHotChocolate%3C%2FSPAN%3E%3C%2FSPAN%3E%3C%2FA%3E%3CSPAN%20class%3D%22TextRun%20SCXW217363184%20BCX8%22%20data-contrast%3D%22auto%22%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%3E)%3C%2FSPAN%3E%3CSPAN%20class%3D%22NormalTextRun%20SCXW217363184%20BCX8%22%3E.%3C%2FSPAN%3E%3C%2FSPAN%3E%3CSPAN%20class%3D%22EOP%20SCXW217363184%20BCX8%22%20data-ccp-props%3D%22%7B%26quot%3B201341983%26quot%3B%3A0%2C%26quot%3B335559739%26quot%3B%3A160%2C%26quot%3B335559740%26quot%3B%3A259%7D%22%3E%26nbsp%3B%3C%2FSPAN%3E%3C%2FP%3E%3C%2FLINGO-TEASER%3E%3CLINGO-LABS%20id%3D%22lingo-labs-3107245%22%20slang%3D%22en-US%22%3E%3CLINGO-LABEL%3EHLS_Hack%3C%2FLINGO-LABEL%3E%3C%2FLINGO-LABS%3E
Co-Authors
Version history
Last update:
‎May 10 2022 05:03 AM
Updated by: