Running Temporal Worker and Workflow and activities inside a single .NET Web Api without seperate Worker

Hi all,
I currently developing a dotnet 8 Web Api project with minimal api endpoints that trigger workflow execution and a seperate worker using Temporal hosting (AddHostedTemporalWorker) where all my activities and workflows live.

Problem
The Web Api need to integrate with EF Core to support the Web api endpoints, but i also need to execute some additional DB operations defined in my activities. What is the best practice to accompish something like this. Should I move all my workflows and activities to the WEB API’s project Program.cs and start the worker from there, as you demonstrate in dotnet samples repository https://github.com/temporalio/sdk-dotnet?

Where the workflows/activities live does not matter too much, but yes if you’re using the hosting extension, the activity constructor has dependency injection where you can add things like a DB client. The activity class is constructed on worker construction. See this sample.

Thanks for you quick answer. Am new to temporal and C#. Yeah having the worker on a different project so far , worked great, until i realized the I need to do DB stuff. The thing is that the web APi will do stuff on the same DB and I was thinking its not a good idea to do DB stuff on the worker as well. I am trying to start a worker in Web Api Program.cs but this does not work ! the worker does not start at all.

Here is my code if you mind having a look.

using Meao.Activities;
using Meao.Models;
using Meao.Requests;
using Meao.Workflows;

using Microsoft.AspNetCore.Mvc;

using Temporalio.Client;
using Temporalio.Worker;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Logging.AddSimpleConsole().SetMinimumLevel(LogLevel.Information);

builder.Services.AddSingleton(ctx =>
    TemporalClient.ConnectAsync(new()
    {
        TargetHost = "localhost:7233",
        LoggerFactory = ctx.GetRequiredService<ILoggerFactory>(),
    }));

// Create a client to localhost on default namespace
// var client = await TemporalClient.ConnectAsync(new("localhost:7233")
// {
//     LoggerFactory = LoggerFactory.Create(builder =>
//         builder.
//             AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
//             SetMinimumLevel(LogLevel.Information)),
// });

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

//app.UseHttpsRedirection();

app.MapPost("/meao/v1/app_instances", async (Task<TemporalClient> clientTask,[FromBody] CreateAppInstRequest? request) =>
{
    app.Logger.LogInformation("Received Create App Instance request");
    
    
  
    
    // call CreateAppInstWF workflow with input parameters
    var client = await clientTask;

    // var handle = await client.StartWorkflowAsync(
    //     (MyWorkflow wf) => wf.RunAsync(model!),
    //     new(id: $"my-workflow-id", taskQueue: MyWorkflow.TaskQueue));

    // Instead of StartWorkflowAsync + GetResultAsync above, 
    // there is an ExecuteWorkflowAsync extension method that is clearer if the handle is not needed

    // Call to execute CreateAppInstWF workflow from Worker
    // TODO: Pass the createRequestDetails object to the RunAsync
    return await client.ExecuteWorkflowAsync(
        (CreateAppInstWF wf) => wf.RunAsync(request!),
        new(id: $"createAppInst-wf-{Guid.NewGuid()}", taskQueue: CreateAppInstWF.TaskQueue)
    );
    
    // var response = new Dictionary<string, object> { { "workflowId", workflowId } };
    // return Results.Ok(response);

    // Return a success response with the new appInstanceId created
    // return new OkObjectResult(appInstance.AppInstanceId);

    // We can also wait on the result using the handle.
    // var status = await handle.GetResultAsync();
    // return Results.Ok(status);
})
.WithTags("Mec Application Orchestrator")
.WithName("Create Application Instance");


app.Run();

async Task RunWorkerAsync()
{
    var client = await TemporalClient.ConnectAsync(new("localhost:7233")
    {
        LoggerFactory = LoggerFactory.Create(builder =>
            builder.
                AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
                SetMinimumLevel(LogLevel.Information)),
    });
    // Cancellation token cancelled on ctrl+c
    using var tokenSource = new CancellationTokenSource();
    Console.CancelKeyPress += (_, eventArgs) =>
    {
        tokenSource.Cancel();
        eventArgs.Cancel = true;
    };

    using var worker = new TemporalWorker(
        client,
        new TemporalWorkerOptions(taskQueue: CreateAppInstWF.TaskQueue)
        .AddAllActivities(new CatalogueActivities())
        .AddAllActivities(new ApmActivities())
        .AddWorkflow<CreateAppInstWF>()
    );

    try
    {
        await worker.ExecuteAsync(tokenSource.Token);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Worker cancelled");
    }
}

await RunWorkerAsync();

Do you think there is an elegant way to accomplish that or propose a fix for the above code?

So it’s definitely a good idea to do DB stuff in activities. But now I realize your original question is about whether the worker should be in the same process as the web app. Usually you want it separate because workers scale differently than web applications, but you can keep it in the same if you want. That doesn’t mean the worker can’t use the same DB as the web app.

No, app.Run(); blocks. If you must colocate these, you’ll want something like the hosting extension and attach to the same builder. So before builder.Build() have something like:

using Temporalio.Extensions.Hosting;

// ...

builder.Host.ConfigureServices(ctx =>
    ctx.AddHostedTemporalWorker(
        clientTargetHost: "localhost:7233",
        clientNamespace: "default",
        taskQueue: MyWorkflow.TaskQueue).
    AddWorkflow<MyWorkflow>());

But you usually don’t want to share web app process and worker process (see this ASP.NET sample).

Thanks Chad !!! Worked like a charm! Did not cross my mind that Temporalio.Extensions.Hosting could be used in WebApplication builder Host!! Yeah you are right about scaling , for the time being it’s just a PoC , but definitely I will migrate some DB Activities/calls to the Worker in the future.