Starts in:
01 DAYS
01 HRS
01 MIN
01 SEC
Contact Us

Integrating ELSA with ASP.NET Zero (Angular)

Elsa is a great open source .NET Standard workflows library. Elsa allows creation of workflows by code, by JSON definition or by its workflow designer UI. In this document, we will basically integrate Elsa with ASP.NET Zero and create a simple workflow following official Elsa Dashboard + Server document.

Fully integrated Elsa sample project can be found on GitHub.

Warning: In ASP.NET Zero versions prior to v13.0.0, Angular 16 is used. Elsa v2 is compatible with Angular 16, so it works seamlessly with these versions. However, when upgrading to newer versions of ASP.NET Zero, you should consider Elsa v2's compatibility.

Create Project

In order to start integrating Elsa with ASP.NET Zero, first follow ASP.NET Zero's Getting Started document and create a new project.

Configuring Host Project

Adding Elsa Packages

After creating the empty project, we need to add required Elsa NuGet packages to our project. Elsa document uses Elsa.Persistence.EntityFramework.Sqlite for persistence but we will use Elsa.Persistence.EntityFramework.SqlServer because ASP.NET Zero use SQL Server by default. So, add packages below to your *.Web.Host project.

ConfigureServices Method

After adding Elsa NuGet packages, we need to configure Elsa in Startup.cs. So, open Startup.cs under .Web.Host/Startup/ and configure it as shown below;

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    ConfigureElsa(services);
    
    // Existing code blocks
}
private void ConfigureElsa(IServiceCollection services)
{
    services.AddElsa(elsa =>
        {
            elsa
            .UseEntityFrameworkPersistence(ef =>
                ef.UseSqlServer(_appConfiguration.GetConnectionString("Default"))
            )
            .AddConsoleActivities()
            .AddHttpActivities(_appConfiguration.GetSection("Elsa").GetSection("Server").Bind)
            .AddQuartzTemporalActivities()
            .AddJavaScriptActivities()
            .AddWorkflowsFrom<Startup>();
            
            elsa.UseAutoMapper(() => { });
        }
    );
    
    // Elsa API endpoints.
    services.AddZeroElsaApiEndpoints();
    services.Configure<ApiVersioningOptions>(options =>
    {
        options.UseApiBehavior = false;
    });
    
    services.Configure<RouteOptions>(options =>
    {
        options.LowercaseUrls = false;
    });

    // For Elsa Dashboard.
    services.AddRazorPages();
    
    // Remove ABP filter and add Elsa special filter
	services.PostConfigure<MvcOptions>(options =>
	{
    	ReplaceResultFilter(options);
	});
}

In this configuration, we are configuring Elsa to use existing ASP.NET Zero database and we configure Elsa with Default connection string. In order for this configuration to work, we need to add below section to appsettings.json.

"Elsa": {
    "Server": {
      "BaseUrl": "https://localhost:44301"
    }
}

This will allow Elsa to use ASP.NET Zero application for handling HTTP activities.

There are some special configuration points here which doesn't exist in default Elsa document. Let's take a look at those sections one by one;

AutoMapper

elsa.UseAutoMapper(() => { });

Both Elsa and ASP.NET Boilerplate creates a MapperConfiguration and uses it to create an IMapper. Because of this, if we don't add this line to Elsa configuration, the app will only use Elsa's AutoMapper configuration. But, after making this configuration, we need to configure Elsa's AutoMapper mappings in our app manually. In order to do this, go to *WebHostModule and add below lines to its PreInitialize method.

// ELSA AutoMapper
Configuration.Modules.AbpAutoMapper().Configurators.Add(config =>
{
    // AutoMapperProfile
    config.CreateMap<IWorkflowBlueprint, WorkflowBlueprintModel>();
    config.CreateMap<IWorkflowBlueprint, WorkflowBlueprintSummaryModel>();
    config.CreateMap<IActivityBlueprint, ActivityBlueprintModel>().ConvertUsing<ActivityBlueprintConverter>();
    config.CreateMap<ICompositeActivityBlueprint, CompositeActivityBlueprintModel>();
    config.CreateMap<IConnection, ConnectionModel>().ConvertUsing<ConnectionConverter>();
    config.CreateMap<WorkflowInstance, WorkflowInstanceSummaryModel>();
    config.CreateMap<WorkflowDefinition, WorkflowDefinitionSummaryModel>();
    config.CreateMap<WorkflowDefinition, WorkflowDefinitionVersionModel>();
    
    // CloningProfile
    config.CreateMap<WorkflowDefinition, WorkflowDefinition>();
    config.CreateMap<WorkflowInstance, WorkflowInstance>();
});

API Versioning

services.AddElsaApiEndpoints();
services.Configure<ApiVersioningOptions>(options =>
{
    options.UseApiBehavior = false;
});

services.Configure<RouteOptions>(options =>
{
    options.LowercaseUrls = false;
});

ASP.NET Zero doesn't support API versioning and Elsa uses it by default. But, disabling UseApiBehavior makes ASP.NET Zero and Elsa to work together. AddElsaApiEndpoints configures LowercaseUrls of RouteOptions to true but ASP.NET Zero requires case sensitive URLs. So, we are converting LowercaseUrls of RouteOptions to false.

Result Filtering

services.PostConfigure<MvcOptions>(options =>
{
    ReplaceResultFilter(options);
});

ASP.NET Zero wraps JSON results by default and adds some additional fields to JSON result. The actual result is accessed by result property of the returned JSON result. But, Elsa doesn't work this way, so we should ignore result wrapping for Elsa's controllers. Unfortunately, ASP.NET Zero doesn't provide an easy configuration for doing that. Because of that, we need to replace default result filter with our own custom result filter using the configuration above.

Here is the content of ReplaceResultFilter method;

private void ReplaceResultFilter(MvcOptions options)
{
    for (var index = options.Filters.Count - 1; index >= 0; --index)
    {
        if (options.Filters[index] is ServiceFilterAttribute &&
            ((ServiceFilterAttribute) options.Filters[index]).ServiceType != typeof(AbpResultFilter))
        {
            continue;
        }

        options.Filters.Insert(index, new ServiceFilterAttribute(typeof(ElsaResultFilter))
        {
            Order = 0
        });

        options.Filters.RemoveAt(index + 1);

        break;
    }
}

This method replaces AbpResultFilter with ElsaResultFilter. Here is the content of ElsaResultFilter;

using System.Linq;
using System.Reflection;
using Abp.AspNetCore.Configuration;
using Abp.AspNetCore.Mvc.Extensions;
using Abp.AspNetCore.Mvc.Results.Wrapping;
using Abp.Dependency;
using Abp.Reflection.Extensions;
using Elsa.Server.Api.Endpoints.Activities;
using Microsoft.AspNetCore.Mvc.Filters;

namespace ElsaAngularDemo.Web.Startup;

public class ElsaResultFilter : IResultFilter, ITransientDependency
{
    private readonly IAbpAspNetCoreConfiguration _configuration;
    private readonly IAbpActionResultWrapperFactory _actionResultWrapperFactory;

    public ElsaResultFilter(IAbpAspNetCoreConfiguration configuration, 
        IAbpActionResultWrapperFactory actionResultWrapper)
    {
        _configuration = configuration;
        _actionResultWrapperFactory = actionResultWrapper;
    }

    public virtual void OnResultExecuting(ResultExecutingContext context)
    {
        if (!context.ActionDescriptor.IsControllerAction())
        {
            return;
        }

        var controller = context.Controller;
        controller.GetType().GetAssembly();

        if (controller.GetType().Assembly == typeof(List).Assembly)
        {
            return;
        }
        
        var methodInfo = context.ActionDescriptor.GetMethodInfo();
        
        var wrapResultAttribute =
            GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(
                methodInfo,
                _configuration.DefaultWrapResultAttribute
            );

        if (!wrapResultAttribute.WrapOnSuccess)
        {
            return;
        }

        _actionResultWrapperFactory.CreateFor(context).Wrap(context);
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
    }
    
    public TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<TAttribute>(MemberInfo memberInfo, TAttribute defaultValue = default(TAttribute), bool inherit = true)
        where TAttribute : class
    {
        return memberInfo.GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
               ?? memberInfo.ReflectedType?.GetTypeInfo().GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
               ?? defaultValue;
    }
}

Configure Method

The additions of Configure method are shown in the code block below;

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
    // existing code blocks

    app.UseStaticFiles();
    
    // ELSA
    app.UseHttpActivities();
    
    app.UseRouting();
    
    // existing code blocks
}

There is one additions here;

  1. app.UseHttpActivities(); this line allows Elsa to handle HTTP request and execute if there are any workflows related to HTTP activities.

Elsa Controllers

By default Elsa controllers are not registered in ASP.NET Zero's dependency injection system. In order to do that, we must register Elsa Conrollers as shown below in the Initialize method of the Host Web Module;

public override void Initialize()
{
    // existing code blocks

    // Register controllers inside ELSA
    Register(typeof(Elsa.Server.Api.Endpoints.WorkflowDefinitions.List).GetAssembly());
    Register(typeof(Elsa.Server.Api.Endpoints.WorkflowDefinitions.Save).GetAssembly());
    Register(typeof(Elsa.Server.Authentication.Controllers.ElsaUserInfoController).GetAssembly());
}

private void Register(Assembly assembly)
{
    //Controller
    IocManager.IocContainer.Register(
        Classes.FromAssembly(assembly)
            .BasedOn<Controller>()
            .If(type => !type.GetTypeInfo().IsGenericTypeDefinition && !type.IsAbstract)
            .LifestyleTransient()
    );

    IocManager.IocContainer.Register(
        Classes.FromAssembly(assembly)
            .BasedOn<ControllerBase>()
            .If(type => !type.GetTypeInfo().IsGenericTypeDefinition && !type.IsAbstract)
            .LifestyleTransient()
    );

    //Razor Pages
    IocManager.IocContainer.Register(
        Classes.FromAssembly(assembly)
            .BasedOn<PageModel>()
            .If(type => !type.GetTypeInfo().IsGenericTypeDefinition && !type.IsAbstract)
            .LifestyleTransient()
    );

    //ViewComponents
    IocManager.IocContainer.Register(
        Classes.FromAssembly(assembly)
            .BasedOn<ViewComponent>()
            .If(type => !type.GetTypeInfo().IsGenericTypeDefinition)
            .LifestyleTransient()
    );
}

UserManager

During the integration of Elsa, we faced an error of Serialization and Deserialization of IdentityOptions in UserManager's InitializeOptionsAsync method. To overcome this problem, you should override the related method in UserManager.cs as shown below;

public override async Task InitializeOptionsAsync(int? tenantId)
{
    Options = new IdentityOptions
    {
        Password =
        {
            RequiredUniqueChars = _optionsAccessor.Value.Password.RequiredUniqueChars
        },
        Stores =
        {
            ProtectPersonalData = _optionsAccessor.Value.Stores.ProtectPersonalData,
            MaxLengthForKeys = _optionsAccessor.Value.Stores.MaxLengthForKeys
        },
        Tokens =
        {
            AuthenticatorIssuer = _optionsAccessor.Value.Tokens.AuthenticatorIssuer,
            ProviderMap = _optionsAccessor.Value.Tokens.ProviderMap,
            AuthenticatorTokenProvider = _optionsAccessor.Value.Tokens.AuthenticatorTokenProvider,
            ChangeEmailTokenProvider = _optionsAccessor.Value.Tokens.ChangeEmailTokenProvider,
            EmailConfirmationTokenProvider = _optionsAccessor.Value.Tokens.EmailConfirmationTokenProvider,
            PasswordResetTokenProvider = _optionsAccessor.Value.Tokens.PasswordResetTokenProvider,
            ChangePhoneNumberTokenProvider = _optionsAccessor.Value.Tokens.ChangePhoneNumberTokenProvider
        },
        User =
        {
            RequireUniqueEmail = _optionsAccessor.Value.User.RequireUniqueEmail,
            AllowedUserNameCharacters = _optionsAccessor.Value.User.AllowedUserNameCharacters
        },
        ClaimsIdentity =
        {
            EmailClaimType = _optionsAccessor.Value.ClaimsIdentity.EmailClaimType,
            RoleClaimType = _optionsAccessor.Value.ClaimsIdentity.RoleClaimType,
            SecurityStampClaimType = _optionsAccessor.Value.ClaimsIdentity.SecurityStampClaimType,
            UserIdClaimType = _optionsAccessor.Value.ClaimsIdentity.UserIdClaimType,
            UserNameClaimType = _optionsAccessor.Value.ClaimsIdentity.UserNameClaimType
        },
        SignIn =
        {
            RequireConfirmedAccount = _optionsAccessor.Value.SignIn.RequireConfirmedAccount,
            RequireConfirmedEmail = _optionsAccessor.Value.SignIn.RequireConfirmedEmail,
            RequireConfirmedPhoneNumber = _optionsAccessor.Value.SignIn.RequireConfirmedPhoneNumber
        },
        Lockout =
        {
            AllowedForNewUsers = await IsTrueAsync(
                AbpZeroSettingNames.UserManagement.UserLockOut.IsEnabled,
                tenantId
            ),
            DefaultLockoutTimeSpan = TimeSpan.FromSeconds(
                await GetSettingValueAsync<int>(
                    AbpZeroSettingNames.UserManagement.UserLockOut.DefaultAccountLockoutSeconds,
                    tenantId
                )
            ),
            MaxFailedAccessAttempts = await GetSettingValueAsync<int>(
                AbpZeroSettingNames.UserManagement.UserLockOut.MaxFailedAccessAttemptsBeforeLockout,
                tenantId
            )
        }
    };
    
    // Password complexity
    Options.Password.RequireDigit = await GetSettingValueAsync<bool>(
        AbpZeroSettingNames.UserManagement.PasswordComplexity.RequireDigit,
        tenantId
    );

    Options.Password.RequireLowercase = await GetSettingValueAsync<bool>(
        AbpZeroSettingNames.UserManagement.PasswordComplexity.RequireLowercase,
        tenantId
    );

    Options.Password.RequireNonAlphanumeric = await GetSettingValueAsync<bool>(
        AbpZeroSettingNames.UserManagement.PasswordComplexity.RequireNonAlphanumeric,
        tenantId
    );

    Options.Password.RequireUppercase = await GetSettingValueAsync<bool>(
        AbpZeroSettingNames.UserManagement.PasswordComplexity.RequireUppercase,
        tenantId
    );

    Options.Password.RequiredLength = await GetSettingValueAsync<int>(
        AbpZeroSettingNames.UserManagement.PasswordComplexity.RequiredLength,
        tenantId
    );
}

private Task<bool> IsTrueAsync(string settingName, int? tenantId)
{
    return GetSettingValueAsync<bool>(settingName, tenantId);
}

private Task<T> GetSettingValueAsync<T>(string settingName, int? tenantId) where T : struct
{
    return tenantId == null
        ? _settingManager.GetSettingValueForApplicationAsync<T>(settingName)
        : _settingManager.GetSettingValueForTenantAsync<T>(settingName, tenantId.Value);
}

This approach manually creates a IdentityOptions and manually sets its fields one by one. The previous version uses AutoMapper which causes a problem.

Hiding Elsa Controllers from Swagger

Since we are not using API Versioning in ASP.NET Zero, we must remove api-version parameter generated by adding ELSA to ASP.NET Zero. In order to do this, create ZeroElsaServiceCollectionExtensions.cs class as shown below next to Startup.cs.

public static class ZeroElsaServiceCollectionExtensions
{
	public static IServiceCollection AddZeroElsaApiEndpoints(this IServiceCollection services,
		Action<ElsaApiOptions>? configureApiOptions = default)
	{
		var apiOptions = new ElsaApiOptions();
		configureApiOptions?.Invoke(apiOptions);

		var setupNewtonsoftJson = apiOptions.SetupNewtonsoftJson ?? (_ => { });

		services.AddControllers().AddNewtonsoftJson(setupNewtonsoftJson);
		services.AddRouting(options => { options.LowercaseUrls = true; });

		 // services.AddVersionedApiExplorer(o =>
		 // {
		 //     o.GroupNameFormat = "'v'VVV";
		 //     o.SubstituteApiVersionInUrl = false;
		 // });

		services.AddApiVersioning(
			options =>
			{
				options.UseApiBehavior = false;
				options.ReportApiVersions = false;
				options.DefaultApiVersion = ApiVersion.Default;
				options.AssumeDefaultVersionWhenUnspecified = true;
			});

		services
			.AddSingleton<ConnectionConverter>()
			.AddSingleton<ActivityBlueprintConverter>()
			.AddScoped<IWorkflowBlueprintMapper, WorkflowBlueprintMapper>()
			.AddSingleton<IEndpointContentSerializerSettingsProvider, EndpointContentSerializerSettingsProvider>()
			.AddAutoMapperProfile<AutoMapperProfile>()
			.AddSignalR();

		return services;
	}
}

Notice that AddVersionedApiExplorer call is commented out. Then, use this extension method AddZeroElsaApiEndpoints instead of AddElsaApiEndpoints in your Startup.cs;

// Elsa API endpoints.
services.AddZeroElsaApiEndpoints();
services.Configure<ApiVersioningOptions>(options =>
{
    options.UseApiBehavior = false;
});

Then, go to SwaggerEnumParameterFilter.cs and add below if statement as the fist line to Apply method.

public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
	if (context.ApiParameterDescription == null || context.ApiParameterDescription.Type == null)
	{
		return;
	}
	
	// existing code blocks
}

Then, go to SwaggerOperationFilter.cs and change the line below;

var enumType = context?.ApiDescription?.ParameterDescriptions[i]?.ParameterDescriptor?.ParameterType;

to this one;

var enumType = context?.ApiDescription?.ParameterDescriptions[i]?.ParameterDescriptor?.ParameterType;
if (enumType == null)
{
	continue;
}

Finally, create a file named SwaggerConfigExtensions.cs in the Web.Core project under swagger folder and copy the content below;

using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Intelly.Web.Swagger
{
    public static class SwaggerConfigExtensions
    {
        public static void AddSwaggerRouteFilter(this SwaggerGenOptions options)
        {
            options.DocInclusionPredicate((docName, apiDesc) =>
            {
                var routeTemplate = apiDesc.RelativePath;

                if (routeTemplate == "workflow-definitions")
                    return false;
                else if (routeTemplate.Contains("workflow-definitions/import-files"))
                    return false;
                else if (routeTemplate.Contains("workflow-instances"))
                    return false;
                else if (routeTemplate.Contains("workflow-registry"))
                    return false;
                else if (routeTemplate.Contains("workflows"))
                    return false;
                else if (routeTemplate.Contains("v{apiVersion}"))
                    return false;
                else if (routeTemplate.Contains("v{version}"))
                    return false;

                return true;
            });
        }
    }
}

and, use this filter to hide Elsa endpoints in swagger document, so Swahsbuckle will not try to generate Typescript proxies for Elsa services. Add the change below in Startup.cs;

services.AddSwaggerGen(options =>
{
	// existing configuration
	 options.AddSwaggerRouteFilter();
});

Configuring Angular Project

Adding NPM Packages

In order to use Elsa Dashboard on our Angular app, add @elsa-workflows/elsa-workflows-studio NPM package to your Angular project.

To copy some of the scripts/styles we will use, add items below to assets section of the angular.json file;

  {
    "glob": "**/*",
    "input": "node_modules/monaco-editor/min",
    "output": "./assets/monaco"
  },
  {
    "glob": "**/*",
    "input": "node_modules/@elsa-workflows/elsa-workflows-studio/dist/elsa-workflows-studio/assets",
    "output": "./assets/elsa-workflows-studio/"
  },
  {
    "glob": "*.css",
    "input": "node_modules/@elsa-workflows/elsa-workflows-studio/dist/elsa-workflows-studio",
    "output": "./assets/elsa-workflows-studio/"
  },
  {
    "glob": "*.js",
    "input": "node_modules/@elsa-workflows/elsa-workflows-studio/dist/elsa-workflows-studio",
    "output": "./assets/elsa-workflows-studio/"
  },
  {
    "glob": "*.png",
    "input": "node_modules/@elsa-workflows/elsa-workflows-studio/dist/elsa-workflows-studio/assets",
    "output": "./assets"
  },

Finally, add items below to index.html file to use Elsa Workflow Studio and Monaco Editor related style and script files;

<link rel="icon" type="image/png" sizes="32x32" href="/assets/elsa-workflows-studio/images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/elsa-workflows-studio/images/favicon-16x16.png" />
<link rel="stylesheet" href="/assets/elsa-workflows-studio/fonts/inter/inter.css" />
<link rel="stylesheet" href="/assets/elsa-workflows-studio/elsa-workflows-studio.css" />

Creating Elsa Component

Before creating the Elsa component which will host Elsa Dashboard, let's create Elsa Module on our Angular app. To do this, create a folder named elsa under the app\admin folder.

Then, create the Elsa Module as shown below under the newly created elsa folder named elsa.module.ts.

import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { ElsaRoutingModule } from './elsa-routing.module';
import { AdminSharedModule } from '@app/admin/shared/admin-shared.module';
import { AppSharedModule } from '@app/shared/app-shared.module';
import { ElsaComponent } from './elsa.component';

@NgModule({
    declarations: [ElsaComponent],
    imports: [AppSharedModule, AdminSharedModule, ElsaRoutingModule],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ElsaModule {}

Now, we can create elsa.component.ts as shown below;

import { AfterViewChecked, Component, OnInit } from "@angular/core";
import { appModuleAnimation } from "@shared/animations/routerTransition";
import { AppComponentBase } from "@shared/common/app-component-base";
import { ScriptLoaderService } from '@shared/utils/script-loader.service';

@Component({
    templateUrl: './elsa.component.html',
    animations: [appModuleAnimation()],
})
export class ElsaComponent extends AppComponentBase implements OnInit, AfterViewChecked {
    
    ngOnInit(): void {
        new ScriptLoaderService().load('/assets/monaco/vs/loader.js');
    }

    ngAfterViewChecked(): void {
        const elsaStudioRoot = document.querySelector('elsa-studio-root');

        elsaStudioRoot.addEventListener('initializing', e => {
            const elsaStudio = (e as any).detail;
            elsaStudio.pluginManager.registerPlugin((window as any).AuthPlugin);
        });
    }
}

And, crate elsa.component.html as shown below;

<div class="container-fluid">
	<div>
		<elsa-studio-root server-url="https://localhost:44301/" monaco-lib-path="assets/monaco">
			<elsa-studio-dashboard></elsa-studio-dashboard>
		</elsa-studio-root>
    </div>
</div>

Finally, create elsa-routing.module.ts to define routing for Elsa Dashboard;

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ElsaComponent } from './elsa.component';

const routes: Routes = [
    {
        path: '',
        component: ElsaComponent,
        pathMatch: 'full',
    },
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule],
})
export class ElsaRoutingModule {}

Adding Elsa Component to Application

In order to use the Elsa component we have created, we need to add it to our Angular application. Since ASP.NET Zero lazy loads the modules, we need to add Elsa module to our Admin routing module. To do this, modify admin-routing.module.ts as shown below;

{
	path: 'elsa',
	loadChildren: () => import('./elsa/elsa.module').then((m) => m.ElsaModule),
	data: { permission: 'Pages.Administration.Roles' },
},

Add line below to typings.d.ts;

declare let monaco: any;

In order to use monaco editor in our angular app, add below items to admin.module.ts;

// existing imports
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@NgModule({
    imports: [
        // existing imports
    ],
    declarations: [],
    exports: [],
    providers: [
        existing providers
    ],
    ], 
	schemas: [CUSTOM_ELEMENTS_SCHEMA] // ELSA integration
})
export class AdminModule {}

Add CUSTOM_ELEMENTS_SCHEMA to app.module.ts as shown below;

// existing imports
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@NgModule({
    declarations: [
        // existing declerations
    ],
    providers: [
        // existing providers
    ],
    imports: [
        // existing imports
    ]
    ],
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}

Finally, modify main.ts and lines below;

import { defineCustomElements as elsaCustomElements } from '@elsa-workflows/elsa-workflows-studio/loader';

elsaCustomElements();

Add Elsa Dashboard to Menu

In order to show Elsa on the main application menu, add item below to app-navigation.service.ts;

new AppMenuItem('Workflows', null, 'flaticon2-setup', '/app/admin/elsa'),

Authentication of Elsa Dsahboard

Elsa's Dashboard uses its own http client to call the server APIs. So, we need to send ASP.NET Zero's authentication token with Elsa's HTTP requests to server. In order to do this, we will be using Elsa Studio's custom plugin approach. For more info, you can check its documentation.

So, we need to add code block below to index.html file;

<script>
	window.AuthPlugin = function (elsaStudio) {
		const { eventBus } = elsaStudio;

		const getAccessToken = async () => {
			return abp.auth.getToken();
		};

		const configureAuthMiddleware = async (e) => {
			const token = await getAccessToken();

			if (!token) return;

			e.service.register({
				onRequest(request) {
					request.headers = { Authorization: `Bearer ${token}` };
					if(request.method === 'post'){
						request.headers['Content-Type'] = 'application/json';
					}
					return request;
				},
			});
		};

		// Handle the "http-client-created" event so we con configure the http client.
		eventBus.on('http-client-created', configureAuthMiddleware);
	};
</script>

Styling

There is a minor style problem when using Elsa Dashboard in ASP.NET Zero's Angular app. In order to fix this problem, add style below to layout.less file;

elsa-modal-dialog .elsa-fixed {
    margin-top: 120px !important;
}

That's all. You can now run the application and navigate to Elsa Dashboard page by clicking the Elsa menu item.

Sample Workflow

You can create a sample workflow as explained in Elsa Documentation. This sample workflow starts with a request to /hello-world endpoint and returns the specified HTTP response.