Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Processes - Cloud Events supporting components for emitting events + MicrosoftGraph emit Demo #9712

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<PackageVersion Include="FastBertTokenizer" Version="1.0.28" />
<PackageVersion Include="PdfPig" Version="0.1.9" />
<PackageVersion Include="Pinecone.NET" Version="2.1.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Memory.Data" Version="8.0.1" />
Expand Down
9 changes: 9 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OllamaFunctionCalling", "sa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAIRealtime", "samples\Demos\OpenAIRealtime\OpenAIRealtime.csproj", "{6154129E-7A35-44A5-998E-B7001B5EDE14}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProcessWithCloudEvents", "samples\Demos\ProcessWithCloudEvents\ProcessWithCloudEvents.csproj", "{36E94769-8A05-4009-808C-E23A0FD2A0F0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1141,6 +1143,12 @@ Global
{6154129E-7A35-44A5-998E-B7001B5EDE14}.Publish|Any CPU.Build.0 = Debug|Any CPU
{6154129E-7A35-44A5-998E-B7001B5EDE14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6154129E-7A35-44A5-998E-B7001B5EDE14}.Release|Any CPU.Build.0 = Release|Any CPU
{36E94769-8A05-4009-808C-E23A0FD2A0F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36E94769-8A05-4009-808C-E23A0FD2A0F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36E94769-8A05-4009-808C-E23A0FD2A0F0}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{36E94769-8A05-4009-808C-E23A0FD2A0F0}.Publish|Any CPU.Build.0 = Debug|Any CPU
{36E94769-8A05-4009-808C-E23A0FD2A0F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36E94769-8A05-4009-808C-E23A0FD2A0F0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1297,6 +1305,7 @@ Global
{B35B1DEB-04DF-4141-9163-01031B22C5D1} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
{481A680F-476A-4627-83DE-2F56C484525E} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{6154129E-7A35-44A5-998E-B7001B5EDE14} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{36E94769-8A05-4009-808C-E23A0FD2A0F0} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
Expand Down
58 changes: 58 additions & 0 deletions dotnet/samples/Demos/ProcessWithCloudEvents/AppConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.

internal sealed class AppConfig
{
/// <summary>
/// The configuration for the Azure EntraId authentication.
/// </summary>
public AzureEntraIdConfig? AzureEntraId { get; set; }

/// <summary>
/// Ensures that the configuration is valid.
/// </summary>
internal void Validate()
{
ArgumentNullException.ThrowIfNull(this.AzureEntraId?.ClientId, nameof(this.AzureEntraId.ClientId));
ArgumentNullException.ThrowIfNull(this.AzureEntraId?.TenantId, nameof(this.AzureEntraId.TenantId));

if (this.AzureEntraId.InteractiveBrowserAuthentication)
{
ArgumentNullException.ThrowIfNull(this.AzureEntraId.InteractiveBrowserRedirectUri, nameof(this.AzureEntraId.InteractiveBrowserRedirectUri));
}
else
{
ArgumentNullException.ThrowIfNull(this.AzureEntraId?.ClientSecret, nameof(this.AzureEntraId.ClientSecret));
}
}

internal sealed class AzureEntraIdConfig
{
/// <summary>
/// App Registration Client Id
/// </summary>
public string? ClientId { get; set; }

/// <summary>
/// App Registration Tenant Id
/// </summary>
public string? TenantId { get; set; }

/// <summary>
/// The client secret to use for the Azure EntraId authentication.
/// </summary>
/// <remarks>
/// This is required if InteractiveBrowserAuthentication is false. (App Authentication)
/// </remarks>
public string? ClientSecret { get; set; }

/// <summary>
/// Specifies whether to use interactive browser authentication (Delegated User Authentication) or App authentication.
/// </summary>
public bool InteractiveBrowserAuthentication { get; set; }

/// <summary>
/// When using interactive browser authentication, the redirect URI to use.
/// </summary>
public string? InteractiveBrowserRedirectUri { get; set; } = "http://localhost";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Process.Models;
using ProcessWithCloudEvents.Processes;
using ProcessWithCloudEvents.Processes.Steps;

namespace ProcessWithCloudEvents.Controllers;
public abstract class CounterBaseController : ControllerBase
{
internal Kernel Kernel { get; init; }
internal KernelProcess Process { get; init; }

private static readonly JsonSerializerOptions s_jsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};

internal Kernel BuildKernel(GraphServiceClient? graphClient = null)
{
var builder = Kernel.CreateBuilder();
if (graphClient != null)
{
builder.Services.AddSingleton<GraphServiceClient>(graphClient);
}
return builder.Build();
}

internal KernelProcess InitializeProcess(ProcessBuilder process)
{
this.InitializeStateFile(process.Name);
var processState = this.LoadProcessState(process.Name);
return process.Build(processState);
}

private string GetTemporaryProcessFilePath(string processName)
{
return Path.Combine(Path.GetTempPath(), $"{processName}.json");
}

internal void InitializeStateFile(string processName)
{
// Initialize the path for the temporary file
var tempProcessFile = this.GetTemporaryProcessFilePath(processName);

// If the file does not exist, create it and initialize with zero
if (!System.IO.File.Exists(tempProcessFile))
{
System.IO.File.WriteAllText(tempProcessFile, "");
}
}

internal void SaveProcessState(string processName, KernelProcessStateMetadata processStateInfo)
{
var content = JsonSerializer.Serialize<KernelProcessStateMetadata>(processStateInfo, s_jsonOptions);
System.IO.File.WriteAllText(this.GetTemporaryProcessFilePath(processName), content);
}

internal KernelProcessStateMetadata? LoadProcessState(string processName)
{
try
{
using StreamReader reader = new(this.GetTemporaryProcessFilePath(processName));
var content = reader.ReadToEnd();
return JsonSerializer.Deserialize<KernelProcessStateMetadata>(content, s_jsonOptions);
}
catch (Exception)
{
return null;
}
}

internal void StoreProcessState(KernelProcess process)
{
var stateMetadata = process.ToProcessStateMetadata();
this.SaveProcessState(process.State.Name, stateMetadata);
}

internal KernelProcessStepState<CounterStepState>? GetCounterState(KernelProcess process)
{
// TODO: Replace when there is a better way of extracting snapshot of local state
return process.Steps
.First(step => step.State.Name == RequestCounterProcess.StepNames.Counter).State as KernelProcessStepState<CounterStepState>;
}

internal async Task<KernelProcess> StartProcessWithEventAsync(string eventName, object? eventData = null)
{
var runningProcess = await this.Process.StartAsync(this.Kernel, new() { Id = eventName, Data = eventData });
var processState = await runningProcess.GetStateAsync();
this.StoreProcessState(processState);

return processState;
}

public virtual async Task<int> IncreaseCounterAsync()
{
return await Task.FromResult(0);
}

public virtual async Task<int> DecreaseCounterAsync()
{
return await Task.FromResult(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;
using ProcessWithCloudEvents.Processes;

namespace ProcessWithCloudEvents.Controllers;
[ApiController]
[Route("[controller]")]
public class CounterWithCloudStepsController : CounterBaseController
{
private readonly ILogger<CounterWithCloudStepsController> _logger;

public CounterWithCloudStepsController(ILogger<CounterWithCloudStepsController> logger, GraphServiceClient graphClient)
{
this._logger = logger;

this.Kernel = this.BuildKernel(graphClient);
this.Process = this.InitializeProcess(RequestCounterProcess.CreateProcessWithCloudSteps());
}

[HttpGet("increase", Name = "IncreaseWithCloudSteps")]
public override async Task<int> IncreaseCounterAsync()
{
var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.IncreaseCounterRequest);
var runningProcess = await this.StartProcessWithEventAsync(eventName);
var counterState = this.GetCounterState(runningProcess);

return counterState?.State?.Counter ?? -1;
}

[HttpGet("decrease", Name = "DecreaseWithCloudSteps")]
public override async Task<int> DecreaseCounterAsync()
{
var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.DecreaseCounterRequest);
var runningProcess = await this.StartProcessWithEventAsync(eventName);
var counterState = this.GetCounterState(runningProcess);

return counterState?.State?.Counter ?? -1;
}

[HttpGet("reset", Name = "ResetCounterWithCloudSteps")]
public async Task<int> ResetCounterAsync()
{
var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest);
var runningProcess = await this.StartProcessWithEventAsync(eventName);
var counterState = this.GetCounterState(runningProcess);

return counterState?.State?.Counter ?? -1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;
using ProcessWithCloudEvents.Processes;

namespace ProcessWithCloudEvents.Controllers;
[ApiController]
[Route("[controller]")]
public class CounterWithCloudSubscribersController : CounterBaseController
{
private readonly ILogger<CounterWithCloudStepsController> _logger;

public CounterWithCloudSubscribersController(ILogger<CounterWithCloudStepsController> logger, GraphServiceClient graphClient)
{
this._logger = logger;
this.Kernel = this.BuildKernel();

var serviceProvider = new ServiceCollection()
.AddSingleton<GraphServiceClient>(graphClient)
.BuildServiceProvider();
this.Process = this.InitializeProcess(RequestCounterProcess.CreateProcessWithProcessSubscriber(serviceProvider));
}

[HttpGet("increase", Name = "IncreaseCounterWithCloudSubscribers")]
public override async Task<int> IncreaseCounterAsync()
{
var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.IncreaseCounterRequest);
var runningProcess = await this.StartProcessWithEventAsync(eventName);
var counterState = this.GetCounterState(runningProcess);

return counterState?.State?.Counter ?? -1;
}

[HttpGet("decrease", Name = "DecreaseCounterWithCloudSubscribers")]
public override async Task<int> DecreaseCounterAsync()
{
var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.DecreaseCounterRequest);
var runningProcess = await this.StartProcessWithEventAsync(eventName);
var counterState = this.GetCounterState(runningProcess);

return counterState?.State?.Counter ?? -1;
}

[HttpGet("reset", Name = "ResetCounterWithCloudSubscribers")]
public async Task<int> ResetCounterAsync()
{
var eventName = RequestCounterProcess.GetEventName(RequestCounterProcess.CounterProcessEvents.ResetCounterRequest);
var runningProcess = await this.StartProcessWithEventAsync(eventName);
var counterState = this.GetCounterState(runningProcess);

return counterState?.State?.Counter ?? -1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.

using Azure.Core;
using Azure.Identity;
using Microsoft.Graph;

public static class GraphServiceProvider
{
public static GraphServiceClient CreateGraphService()
{
string[] scopes;

var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) // Set the base path for appsettings.json
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // Load appsettings.json
.AddUserSecrets<Program>()
.AddEnvironmentVariables()
.Build()
.Get<AppConfig>() ??
throw new InvalidOperationException("Configuration is not setup correctly.");

config.Validate();

TokenCredential credential = null!;
if (config.AzureEntraId!.InteractiveBrowserAuthentication) // Authentication As User
{
/// Use this if using user delegated permissions
scopes = ["User.Read", "Mail.Send"];

credential = new InteractiveBrowserCredential(
new InteractiveBrowserCredentialOptions
{
TenantId = config.AzureEntraId.TenantId,
ClientId = config.AzureEntraId.ClientId,
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
RedirectUri = new Uri(config.AzureEntraId.InteractiveBrowserRedirectUri!)
});
}
else // Authentication As Application
{
scopes = ["https://graph.microsoft.com/.default"];

credential = new ClientSecretCredential(
config.AzureEntraId.TenantId,
config.AzureEntraId.ClientId,
config.AzureEntraId.ClientSecret);
}

return new GraphServiceClient(credential, scopes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Graph.Me.SendMail;
using Microsoft.Graph.Models;

namespace ProcessWithCloudEvents.MicrosoftGraph;

public static class GraphRequestFactory
{
public static SendMailPostRequestBody CreateEmailBody(string subject, string content, List<string> recipients)
{
var message = new SendMailPostRequestBody()
{
Message = new Microsoft.Graph.Models.Message()
{
Subject = subject,
Body = new()
{
ContentType = Microsoft.Graph.Models.BodyType.Text,
Content = content,
},
ToRecipients = recipients.Select(address => new Recipient { EmailAddress = new() { Address = address } }).ToList(),
},
SaveToSentItems = true,
};

return message;
}
}
Loading
Loading