Veracity.Common.MessagingPack.Tenant 1.0.0-preview.4

Prefix Reserved
This is a prerelease version of Veracity.Common.MessagingPack.Tenant.
dotnet add package Veracity.Common.MessagingPack.Tenant --version 1.0.0-preview.4
                    
NuGet\Install-Package Veracity.Common.MessagingPack.Tenant -Version 1.0.0-preview.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Veracity.Common.MessagingPack.Tenant" Version="1.0.0-preview.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Veracity.Common.MessagingPack.Tenant" Version="1.0.0-preview.4" />
                    
Directory.Packages.props
<PackageReference Include="Veracity.Common.MessagingPack.Tenant" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Veracity.Common.MessagingPack.Tenant --version 1.0.0-preview.4
                    
#r "nuget: Veracity.Common.MessagingPack.Tenant, 1.0.0-preview.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Veracity.Common.MessagingPack.Tenant@1.0.0-preview.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Veracity.Common.MessagingPack.Tenant&version=1.0.0-preview.4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Veracity.Common.MessagingPack.Tenant&version=1.0.0-preview.4&prerelease
                    
Install as a Cake Tool

Veracity.Common.MessagingPack.Tenant

NuGet License

A comprehensive event handling framework for Veracity Tenant Management events. This package provides strongly-typed event handlers for consuming domain events from Veracity's tenant management system, enabling you to react to changes in tenants, applications, licenses, profiles, and more.

Table of Contents

Overview

The Veracity.Common.MessagingPack.Tenant package simplifies integration with Veracity's tenant management system by providing:

  • Strongly-typed event handlers for all tenant management domain events
  • Automatic event routing and deserialization
  • Dependency injection integration for easy service registration
  • Extensible architecture for custom business logic

This framework is designed for applications that need to react to changes in the Veracity ecosystem, such as:

  • Provisioning resources when an application is installed in a tenant
  • Synchronizing user access when licenses are assigned or revoked
  • Updating local caches when tenant or profile information changes
  • Managing application-specific resources tied to tenant lifecycles

Installation

Install the package via NuGet:

dotnet add package Veracity.Common.MessagingPack.Tenant

Or via Package Manager Console:

Install-Package Veracity.Common.MessagingPack.Tenant

Quick Start

1. Create a Marker Class

Create a class that implements IEventHandlerLoader to mark the assembly containing your event handlers:

using Veracity.Common.MessagingPack.Tenant;

public class MyEventHandlerLoader : IEventHandlerLoader
{
    // This is just a marker class - no implementation needed
}

2. Create an Event Handler

Inherit from one of the abstract event handler classes and implement the required method:

using Microsoft.Extensions.Logging;
using Veracity.Common.MessagingPack.Tenant.Handler;
using Veracity.Common.MessagingPack.Tenant.Models;

public class MyLicenseAddedHandler : LicenseAdded
{
    private readonly ILogger<MyLicenseAddedHandler> _logger;
    private readonly IMyProvisioningService _provisioningService;

    public MyLicenseAddedHandler(
        ILogger<MyLicenseAddedHandler> logger,
 IMyProvisioningService provisioningService)
    {
        _logger = logger;
     _provisioningService = provisioningService;
    }

    public override async Task Add(License addedLicense)
    {
        _logger.LogInformation(
            "License added for user {UserId} in application {AppId}", 
      addedLicense.MemberId, 
     addedLicense.ApplicationId);

        // Your business logic here
        await _provisioningService.GrantAccess(addedLicense);
    }
}

3. Register Services

In your Program.cs or Startup.cs, register the event handlers:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Veracity.Common.MessagingPack.Tenant.Handler.Helpers;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
        
        // Register your event handlers
        services.AddTenantEventHandlers<MyEventHandlerLoader>();
        
        // Register your custom services
        services.AddTransient<IMyProvisioningService, MyProvisioningService>();
    })
    .Build();

host.Run();

4. Create an Azure Function Trigger

Create an Azure Function that receives Service Bus messages and dispatches them to your handlers:

using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Veracity.Common.MessagingPack.Tenant.Handler.Helpers;

public class TenantEventConsumer
{
    private readonly ILogger<TenantEventConsumer> _logger;
    private readonly ICommonEventHandler _eventHandler;

    public TenantEventConsumer(
   ILogger<TenantEventConsumer> logger, 
        ICommonEventHandler eventHandler)
    {
        _logger = logger;
 _eventHandler = eventHandler;
    }

    [Function(nameof(TenantEventConsumer))]
    public async Task Run(
        [ServiceBusTrigger("%QueueName%", Connection = "ServiceBusConnection")] 
        ServiceBusReceivedMessage message)
    {
        _logger.LogInformation(
     "Processing message {MessageId}", 
      message.MessageId);

   // Dispatch the event to the appropriate handler
      await _eventHandler.OnEvent(message.Body);
    }
}

Core Concepts

Event Handler Lifecycle

  1. Event Reception: Your Azure Function receives a Service Bus message
  2. Event Parsing: The ICommonEventHandler deserializes the message into a DomainEvent
  3. Handler Resolution: The framework finds the appropriate handler based on the event type
  4. Event Processing: Your handler's method is invoked with the strongly-typed payload
  5. Completion: The handler completes, and the message is acknowledged

Dependency Injection

All event handlers are registered as transient services in the DI container. This means:

  • Each event invocation gets a fresh handler instance
  • You can inject any registered service into your handler constructors
  • Handler dependencies are resolved automatically

Event Types

Events follow the pattern: com.veracity.{entity}.{action}

Examples:

  • com.veracity.tenant.replace - Tenant updated
  • com.veracity.tenantservice.add - Application installed
  • com.veracity.service.subscription.add - License added
  • com.veracity.profile.delete - Profile removed

Available Event Handlers

Application Events

ApplicationInstalled

Triggered when your application is installed in a new tenant.

public class MyApplicationInstalledHandler : ApplicationInstalled
{
    protected override async Task Install(Application application)
    {
 // Provision resources for the new tenant
        await ProvisionTenantResources(application.TenantId);
    }
}

Event Type: com.veracity.tenantservice.create

Use Cases:

  • Provision tenant-specific resources (databases, storage, etc.)
  • Initialize application configuration for the tenant
  • Set up initial data or templates
  • Register webhooks or callbacks
ApplicationUpdated

Triggered when an application instance is updated.

public class MyApplicationUpdatedHandler : ApplicationUpdated
{
    protected override async Task Update(Application application)
    {
  // Handle application configuration changes
  }
}

Event Type: com.veracity.tenantservice.replace

ApplicationUninstalled

Triggered when your application is uninstalled from a tenant.

public class MyApplicationUninstalledHandler : ApplicationUninstalled
{
    protected override async Task UnInstall(Application application)
 {
     // Clean up tenant resources
        await DeleteTenantResources(application.TenantId);
    }
}

Event Type: com.veracity.tenantservice.delete

Use Cases:

  • Clean up tenant-specific resources
  • Archive or delete tenant data
  • Revoke access and cleanup credentials
  • Remove webhooks or subscriptions
RelayFromDeveloperPortal

Triggered when application configuration changes are made in the Veracity Developer Portal and relayed to tenant management.

public class MyRelayFromDeveloperPortalHandler : RelayFromDeveloperPortal
{
    protected override async Task Relay(PartialApplication relayMessage)
    {
        // Handle developer portal configuration changes
        await UpdateApplicationConfiguration(relayMessage);
    }
}

Event Type: com.veracity.tenantservice.relay.fromDeveloper

Use Cases:

  • Sync application metadata from Developer Portal to tenant installations
  • Update pricing tier information across tenant instances
  • Refresh license count limits
  • Propagate application configuration changes

Key Properties (via PartialApplication):

  • ApplicationId - The service ID from the registry
  • OrderNumber - Purchase order number
  • PricingTier - Pricing tier for the application
  • NumberOfLicenses - Number of licenses (null = unlimited)
  • TenantId (inherited) - The tenant where the update applies
  • Name (inherited) - Application name

License Events

LicenseAdded

Triggered when a license (subscription) is assigned to a user or group.

public class MyLicenseAddedHandler : LicenseAdded
{
    public override async Task Add(License license)
    {
        // Grant user access to your application
    }
}

Event Type: com.veracity.service.subscription.add

Key Properties:

  • MemberId - The user or group ID
  • ApplicationId - Your application's service ID
  • ApplicationInstanceId - The tenant-specific installation ID
  • AccessLevel - The assigned access level/role
  • SubscriptionState - State of the subscription
LicenseUpdated

Triggered when a license is modified (e.g., access level changed).

public class MyLicenseUpdatedHandler : LicenseUpdated
{
    public override async Task Update(License license)
    {
     // Update user permissions
    }
}

Event Type: com.veracity.service.subscription.replace

LicenseDeleted

Triggered when a license is revoked from a user or group.

public class MyLicenseDeletedHandler : LicenseDeleted
{
    public override async Task Remove(License license)
    {
        // Revoke user access
    }
}

Event Type: com.veracity.service.subscription.delete

Profile Events

ProfileCreated

Triggered when a user profile is added to a tenant.

public class MyProfileCreatedHandler : ProfileCreated
{
    protected override async Task Add(Profile profile)
    {
        // Handle new user in tenant
    }
}

Event Type: com.veracity.profile.create

ProfileUpdated

Triggered when a profile is updated (e.g., email change, state change).

public class MyProfileUpdatedHandler : ProfileUpdated
{
    protected override async Task Update(Profile profile)
    {
        // Sync user information
    }
}

Event Type: com.veracity.profile.replace

ProfileRemoved

Triggered when a profile is removed from a tenant.

public class MyProfileRemovedHandler : ProfileRemoved
{
    protected override async Task Remove(Profile profile)
    {
        // Clean up user-specific data
    }
}

Event Type: com.veracity.profile.delete

User Group Events

UserGroupCreated

Triggered when a user group is created in a tenant.

public class MyUserGroupCreatedHandler : UserGroupCreated
{
    protected override async Task Add(UserGroup group)
    {
     // Handle new group
    }
}

Event Type: com.veracity.usergroup.create

UserGroupUpdated

Triggered when a user group is modified.

public class MyUserGroupUpdatedHandler : UserGroupUpdated
{
    protected override async Task Update(UserGroup group)
    {
        // Sync group changes
    }
}

Event Type: com.veracity.usergroup.replace

UserGroupRemoved

Triggered when a user group is deleted.

public class MyUserGroupRemovedHandler : UserGroupRemoved
{
    protected override async Task Remove(UserGroup group)
    {
        // Clean up group resources
    }
}

Event Type: com.veracity.usergroup.delete

Group Membership Events

MemberAdded

Triggered when a user is added to a group.

public class MyMemberAddedHandler : MemberAdded
{
    public override async Task Add(Membership membership)
  {
        // Handle group membership addition
    }
}

Event Type: com.veracity.group.member.add

MemberUpdated

Triggered when group membership is modified.

public class MyMemberUpdatedHandler : MemberUpdated
{
public override async Task Update(Membership membership)
    {
        // Handle membership changes
    }
}

Event Type: com.veracity.group.member.replace

MemberDeleted

Triggered when a user is removed from a group.

public class MyMemberDeletedHandler : MemberDeleted
{
  public override async Task Remove(Membership membership)
    {
        // Handle membership removal
    }
}

Event Type: com.veracity.group.member.delete

Tenant Events

TenantUpdated

Triggered when tenant information is updated.

public class MyTenantUpdatedHandler : TenantUpdated
{
    protected override async Task Update(Tenant tenant)
    {
        // Sync tenant metadata
    }
}

Event Type: com.veracity.tenant.replace

Element Events

Application elements represent resources or assets within your application (e.g., projects, datasets, workspaces).

ElementAdded

Triggered when an application element is created.

public class MyElementAddedHandler : ElementAdded
{
    protected override async Task Add(ApplicationElement element)
    {
        // Handle new element creation
}
}

Event Type: com.veracity.element.add

ElementUpdated

Triggered when an element is modified.

public class MyElementUpdatedHandler : ElementUpdated
{
    protected override async Task Update(ApplicationElement element)
    {
   // Sync element changes
    }
}

Event Type: com.veracity.element.replace

ElementDeleted

Triggered when an element is deleted.

public class MyElementDeletedHandler : ElementDeleted
{
protected override async Task Remove(ApplicationElement element)
    {
        // Clean up element resources
    }
}

Event Type: com.veracity.element.delete

Element Right Events

Element rights represent granular permissions to specific application elements.

ElementRightAdded

Triggered when a user or group is granted access to an element.

public class MyElementRightAddedHandler : ElementRightAdded
{
    protected override async Task Add(ElementRight elementRight)
  {
        // Grant element access
    }
}

Event Type: com.veracity.element.right.add

Key Properties:

  • ElementId - The Veracity element ID
  • ElementExternalId - Your application's element identifier
  • ElementName - Display name of the element
  • All properties from License (MemberId, ApplicationId, AccessLevel, etc.)
ElementRightUpdated

Triggered when element access permissions are modified.

public class MyElementRightUpdatedHandler : ElementRightUpdated
{
    protected override async Task Update(ElementRight elementRight)
 {
      // Update element permissions
    }
}

Event Type: com.veracity.element.right.replace

ElementRightDeleted

Triggered when element access is revoked.

public class MyElementRightDeletedHandler : ElementRightDeleted
{
    protected override async Task Remove(ElementRight elementRight)
  {
        // Revoke element access
    }
}

Event Type: com.veracity.element.right.delete

Event Models

Application

Represents an instance of your application installed in a tenant.

public class Application : TenantPayloadBase
{
    public Guid ApplicationInstanceId { get; }      // Unique installation ID
    public string ApplicationId { get; set; }       // Service ID from registry
    public string PricingTier { get; set; }         // Purchased pricing tier
    public string OrderNumber { get; set; }         // Purchase order number
    public bool AutoAssignSubscription { get; set; } // Auto-assign to all users
    public int? NumberOfLicenses { get; set; }      // License count (null = unlimited)
    public string State { get; set; }       // "pending", "approved"
    public int? SubscriptionCap { get; set; } // Max subscriptions (null = no cap)
    public string ManagementMode { get; set; }    // "legacy", "veracityManaged", "hybrid", "serviceManaged"
    public List<Capability> Capabilities { get; set; } // Additional features
    public string[] AccessLevels { get; set; }  // Available roles
}

PartialApplication

Represents a subset of application information sent when changes are relayed from the Developer Portal.

public class PartialApplication : TenantPayloadBase
{
    public string ApplicationId { get; set; }       // Service ID from registry
    public string OrderNumber { get; set; }         // Purchase order number
    public string PricingTier { get; set; }         // Purchased pricing tier
    public int? NumberOfLicenses { get; set; }      // License count (null = unlimited)
}

Use Case: This lightweight model is used when the Developer Portal notifies tenant management about application configuration updates. It contains only the fields that can be updated from the Developer Portal, such as pricing tiers and license counts.

License

Represents a user or group subscription to an application.

public class License : MembershipCore
{
 public Guid MemberId { get; }        // User or group ID
    public Guid ApplicationInstanceId { get; }      // Installation ID
    public string Application { get; }              // Application name
    public string ApplicationId { get; }            // Service ID
    public string AccessLevel { get; set; }         // Assigned role
    public string SubscriptionState { get; set; }   // Subscription status
    public bool NullLicense { get; set; }  // Placeholder license indicator
}

Profile

Represents a user profile within a tenant.

public class Profile : TenantPayloadBase
{
    public Guid ProfileId { get; }         // Profile ID
    public string Email { get; set; }       // User email
    public bool IsServicePrincipal { get; set; }    // Service account indicator
  public string UserId { get; set; }       // Veracity user ID
public string State { get; set; }            // "active", "pending"
}

Tenant

Represents a tenant (organization/company).

public class Tenant : TenantPayloadBase
{
    public Guid TenantId { get; }        // Tenant ID
    public string LegalEntityId { get; set; }       // Legal entity identifier
    public string LegalEntityName { get; set; }     // Company name
    public bool IsLegacy { get; set; }              // Legacy tenant indicator
    public string[] Domains { get; set; }   // Associated domains
    public string TenantType { get; set; }          // Tenant classification
    public string AffiliationMode { get; set; }     // How users join
    public bool IsDisabled { get; set; }            // Tenant status
    public string DnvCustomerId { get; set; }       // DNV-specific identifier
}

UserGroup

Represents a user group within a tenant.

public class UserGroup : TenantPayloadBase
{
    public Guid GroupId { get; }       // Group ID
    public bool BuiltIn { get; set; }   // System group indicator
}

Membership

Represents a user's membership in a group.

public class Membership : MembershipCore
{
    public Guid MemberId { get; }           // Member (user/group) ID
    public Guid GroupId { get; }           // Group ID
    public string GroupName { get; }        // Group display name
    public string[] ChildrenIds { get; set; }    // Nested groups
    public string[] ParentIds { get; set; }         // Parent groups
}

ApplicationElement

Represents a resource or asset within an application.

public class ApplicationElement : TenantPayloadBase
{
    public Guid ApplicationInstanceId { get; set; } // Installation ID
    public Guid ApplicationId { get; set; }         // Service ID
    public Guid AssetInstanceId { get; }            // Element ID
    public string ElementExternalId { get; set; }   // Your element ID
    public string ElementTypeName { get; set; }     // Element type
    public string ElementIconUrl { get; set; }      // Icon URL
    public string Description { get; set; }         // Element description
    public List<Capability> Capabilities { get; set; } // Additional features
    public List<string> AccessLevels { get; set; }  // Available roles
}

ElementRight

Represents granular access to an application element (extends License).

public class ElementRight : License
{
  public string ElementName { get; set; }         // Element display name
    public string ElementId { get; set; }      // Veracity element ID
    public string ElementExternalId { get; set; }   // Your element ID
}

TenantPayloadBase

Base class for all tenant-related entities.

public abstract class TenantPayloadBase
{
    public Guid TenantId { get; }    // Owning tenant
    public string Name { get; set; }           // Entity name
    public string PrimaryId { get; set; }           // Primary identifier
    public string SecondaryId { get; set; }         // Secondary identifier
    public List<Property> Properties { get; set; }  // Custom properties
}

Configuration

Application Settings

Configure your Azure Function with the following settings:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "ServiceBusConnection": "Endpoint=sb://your-servicebus.servicebus.windows.net/;SharedAccessKeyName=...",
    "QueueName": "veracity-tenant-events"
  }
}

Error Handling Configuration

Configure the event handler behavior:

using Veracity.Common.MessagingPack.Tenant.Handler.Helpers;

// Throw exceptions for unknown events (default: false)
CommonEventHandler.ThrowExceptsionOnUnknownEvents = true;

Usage Examples

Example 1: Application Installation with Resource Provisioning

public class MyApplicationInstalledHandler : ApplicationInstalled
{
    private readonly ILogger<MyApplicationInstalledHandler> _logger;
    private readonly IResourceProvisioningService _provisioning;
    private readonly IVeracityGraphClient _graphClient;

    public MyApplicationInstalledHandler(
        ILogger<MyApplicationInstalledHandler> logger,
        IResourceProvisioningService provisioning,
 IVeracityGraphClient graphClient)
    {
   _logger = logger;
        _provisioning = provisioning;
        _graphClient = graphClient;
    }

    protected override async Task Install(Application application)
    {
 _logger.LogInformation(
            "Installing application for tenant {TenantId}, Instance {InstanceId}",
            application.TenantId,
     application.ApplicationInstanceId);

        try
        {
            // Get tenant details
         var tenant = await _graphClient.Tenants.GetTenant(
       application.TenantId.ToString());

         // Provision tenant-specific resources
            var provisioningResult = await _provisioning.ProvisionTenant(
                tenant, 
                application);

        if (provisioningResult.Failed)
       {
      if (provisioningResult.CanRetry)
         {
        // Throwing will cause message retry
  throw new Exception(
          $"Provisioning failed: {provisioningResult.ErrorMessage}");
     }
          
      _logger.LogError(
      "Provisioning failed permanently: {Error}",
      provisioningResult.ErrorMessage);
             return;
       }

     // Update application state to active
    var app = await _graphClient.ThisApplication.GetApplication(
     application.TenantId.ToString());
   
            await app.MakeJsonPatch()
      .UpdateInstallmentState(InstallmentState.Active)
          .ExecutePatchApplicationAsync();

            _logger.LogInformation(
     "Successfully provisioned application for tenant {TenantId}",
            application.TenantId);
        }
        catch (Exception ex)
        {
    _logger.LogError(ex,
  "Error installing application for tenant {TenantId}",
      application.TenantId);
 throw; // Retry the message
        }
  }
}

Example 2: License Management with Access Control

public class MyLicenseHandler : LicenseAdded
{
    private readonly IAccessControlService _accessControl;
    private readonly IUserSyncService _userSync;
 private readonly INotificationService _notifications;

    public MyLicenseHandler(
        IAccessControlService accessControl,
        IUserSyncService userSync,
        INotificationService notifications)
    {
     _accessControl = accessControl;
        _userSync = userSync;
        _notifications = notifications;
    }

    public override async Task Add(License license)
    {
        // Don't process placeholder licenses
        if (license.NullLicense)
    return;

        // Only process active subscriptions
  if (license.SubscriptionState != "Subscribing")
   return;

        // Sync user information
        await _userSync.SyncUser(license.MemberId, license.TenantId);

        // Grant access with specific role
        await _accessControl.GrantAccess(
   userId: license.MemberId,
         tenantId: license.TenantId,
            applicationId: license.ApplicationInstanceId,
  role: license.AccessLevel);

        // Send welcome notification
        await _notifications.SendWelcomeEmail(
            license.MemberId,
      license.Application);
    }
}

public class MyLicenseDeletedHandler : LicenseDeleted
{
    private readonly IAccessControlService _accessControl;
    private readonly ICleanupService _cleanup;

    public MyLicenseDeletedHandler(
        IAccessControlService accessControl,
        ICleanupService cleanup)
    {
        _accessControl = accessControl;
    _cleanup = cleanup;
    }

    public override async Task Remove(License license)
    {
        // Revoke access immediately
        await _accessControl.RevokeAccess(
    license.MemberId,
 license.ApplicationInstanceId);

 // Schedule cleanup of user data
        await _cleanup.ScheduleUserDataCleanup(
            license.MemberId,
            license.TenantId,
            license.ApplicationInstanceId,
            retentionDays: 30);
  }
}

Example 3: Element Rights Management

public class MyElementRightAddedHandler : ElementRightAdded
{
    private readonly IProjectAccessService _projectAccess;
    private readonly ILogger<MyElementRightAddedHandler> _logger;

    public MyElementRightAddedHandler(
        IProjectAccessService projectAccess,
    ILogger<MyElementRightAddedHandler> logger)
    {
        _projectAccess = projectAccess;
_logger = logger;
    }

    protected override async Task Add(ElementRight elementRight)
    {
        _logger.LogInformation(
            "Granting {AccessLevel} access to element {ElementId} for user {UserId}",
  elementRight.AccessLevel,
    elementRight.ElementExternalId,
            elementRight.MemberId);

    // Grant access to the specific project/resource
   await _projectAccess.GrantProjectAccess(
   projectId: elementRight.ElementExternalId,
 userId: elementRight.MemberId,
      role: MapAccessLevelToRole(elementRight.AccessLevel));
    }

    private ProjectRole MapAccessLevelToRole(string accessLevel)
    {
        return accessLevel?.ToLower() switch
        {
            "admin" => ProjectRole.Owner,
  "editor" => ProjectRole.Editor,
  "viewer" => ProjectRole.Viewer,
   _ => ProjectRole.Viewer
      };
    }
}

Example 4: Multiple Handlers for Complex Workflows

// Handler 1: Update local cache
public class TenantCacheUpdater : TenantUpdated
{
    private readonly ITenantCacheService _cache;

    public TenantCacheUpdater(ITenantCacheService cache)
    {
      _cache = cache;
    }

    protected override async Task Update(Tenant tenant)
    {
        await _cache.UpdateTenant(tenant);
    }
}

// Handler 2: Sync to external system
public class TenantExternalSync : TenantUpdated
{
    private readonly IExternalSystemClient _externalSystem;

    public TenantExternalSync(IExternalSystemClient externalSystem)
    {
     _externalSystem = externalSystem;
    }

    protected override async Task Update(Tenant tenant)
    {
        await _externalSystem.SyncTenant(tenant);
    }
}

// Handler 3: Audit logging
public class TenantAuditLogger : TenantUpdated
{
    private readonly IAuditService _audit;

    public TenantAuditLogger(IAuditService audit)
    {
      _audit = audit;
    }

 protected override async Task Update(Tenant tenant)
    {
        await _audit.LogTenantUpdate(tenant);
    }
}

Example 5: Group Membership with Cascading Permissions

public class MyMemberAddedHandler : MemberAdded
{
    private readonly IGroupService _groupService;
    private readonly IPermissionService _permissions;

    public MyMemberAddedHandler(
        IGroupService groupService,
        IPermissionService permissions)
    {
        _groupService = groupService;
      _permissions = permissions;
    }

    public override async Task Add(Membership membership)
    {
    // Get group information
        var group = await _groupService.GetGroup(
            membership.GroupId,
            membership.TenantId);

        // Apply group-level permissions to the user
     await _permissions.ApplyGroupPermissions(
     userId: membership.MemberId,
         groupId: membership.GroupId,
   tenantId: membership.TenantId);

   // Handle nested groups
        if (membership.ParentIds?.Any() == true)
        {
  foreach (var parentId in membership.ParentIds)
   {
     await _permissions.InheritParentPermissions(
       membership.MemberId,
 Guid.Parse(parentId));
  }
        }
    }
}

Example 6: Developer Portal Configuration Relay

public class MyRelayFromDeveloperPortalHandler : RelayFromDeveloperPortal
{
    private readonly ILogger<MyRelayFromDeveloperPortalHandler> _logger;
    private readonly IApplicationConfigService _configService;
    private readonly ILicenseManagementService _licenseService;

    public MyRelayFromDeveloperPortalHandler(
        ILogger<MyRelayFromDeveloperPortalHandler> logger,
        IApplicationConfigService configService,
        ILicenseManagementService licenseService)
    {
        _logger = logger;
        _configService = configService;
        _licenseService = licenseService;
    }

    protected override async Task Relay(PartialApplication relayMessage)
    {
        _logger.LogInformation(
            "Processing Developer Portal relay for application {AppId} in tenant {TenantId}",
            relayMessage.ApplicationId,
            relayMessage.TenantId);

        try
        {
            // Update application configuration from Developer Portal
            await _configService.UpdateFromDeveloperPortal(
                tenantId: relayMessage.TenantId,
                applicationId: relayMessage.ApplicationId,
                pricingTier: relayMessage.PricingTier,
                orderNumber: relayMessage.OrderNumber,
                applicationName: relayMessage.Name);

            // Handle license count changes
            if (relayMessage.NumberOfLicenses.HasValue)
            {
                await _licenseService.UpdateLicenseCapacity(
                    tenantId: relayMessage.TenantId,
                    applicationId: relayMessage.ApplicationId,
                    licenseCount: relayMessage.NumberOfLicenses.Value);

                _logger.LogInformation(
                    "Updated license capacity to {Count} for app {AppId}",
                    relayMessage.NumberOfLicenses.Value,
                    relayMessage.ApplicationId);
            }

            // Trigger any downstream updates
            await _configService.NotifyConfigurationChanged(
                relayMessage.TenantId,
                relayMessage.ApplicationId);

            _logger.LogInformation(
                "Successfully processed Developer Portal relay");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "Error processing Developer Portal relay for app {AppId}",
                relayMessage.ApplicationId);
            throw; // Allow retry
        }
    }
}

Use Case: This handler synchronizes application configuration changes made in the Veracity Developer Portal across all tenant installations. It's particularly useful for:

  • Updating pricing tier information when a customer upgrades/downgrades
  • Adjusting license counts across tenant instances
  • Propagating application metadata changes to your local systems
  • Maintaining consistency between Developer Portal and tenant installations

Best Practices

1. Idempotency

Design your handlers to be idempotent - they should produce the same result even if called multiple times:

public class IdempotentLicenseHandler : LicenseAdded
{
    private readonly IAccessControlService _accessControl;

    public override async Task Add(License license)
    {
        // Check if access already exists
        var hasAccess = await _accessControl.HasAccess(
         license.MemberId,
            license.ApplicationInstanceId);

 if (!hasAccess)
        {
            await _accessControl.GrantAccess(
   license.MemberId,
       license.ApplicationInstanceId,
         license.AccessLevel);
        }
    }
}

2. Error Handling and Retries

Use structured error handling and let critical errors bubble up for retry:

public class RobustApplicationHandler : ApplicationInstalled
{
    private readonly ILogger<RobustApplicationHandler> _logger;

    protected override async Task Install(Application application)
    {
    try
    {
        await ProvisionResources(application);
    }
        catch (TransientException ex)
        {
     _logger.LogWarning(ex, "Transient error, will retry");
      throw; // Allow retry
        }
    catch (PermanentException ex)
  {
    _logger.LogError(ex, "Permanent error, skipping");
      // Don't throw - message will be processed
        }
    }
}

3. Logging

Include contextual information in your logs:

public class WellLoggedHandler : LicenseAdded
{
    private readonly ILogger<WellLoggedHandler> _logger;

    public override async Task Add(License license)
  {
using (_logger.BeginScope(new Dictionary<string, object>
        {
    ["TenantId"] = license.TenantId,
            ["MemberId"] = license.MemberId,
            ["ApplicationId"] = license.ApplicationId,
            ["EventType"] = "LicenseAdded"
        }))
        {
_logger.LogInformation("Processing license addition");
       
       try
   {
 await ProcessLicense(license);
       _logger.LogInformation("Successfully processed license");
}
     catch (Exception ex)
     {
_logger.LogError(ex, "Failed to process license");
     throw;
     }
        }
    }
}

4. Separation of Concerns

Keep handlers focused on event processing logic:

public class FocusedLicenseHandler : LicenseAdded
{
    private readonly ILicenseProcessor _processor;

    public FocusedLicenseHandler(ILicenseProcessor processor)
    {
        _processor = processor;
 }

    public override async Task Add(License license)
  {
      // Handler just delegates to business logic
        await _processor.ProcessLicenseAddition(license);
    }
}

// Business logic in a separate service
public class LicenseProcessor : ILicenseProcessor
{
    public async Task ProcessLicenseAddition(License license)
{
   // Complex business logic here
    }
}

5. Testing

Make your handlers testable by using interfaces:

// Production handler
public class ProductionLicenseHandler : LicenseAdded
{
    private readonly IAccessControlService _accessControl;

 public ProductionLicenseHandler(IAccessControlService accessControl)
    {
        _accessControl = accessControl;
    }

    public override async Task Add(License license)
    {
        await _accessControl.GrantAccess(license.MemberId, license.ApplicationInstanceId);
    }
}

// Unit test
[Fact]
public async Task LicenseAdded_GrantsAccess()
{
    // Arrange
    var mockAccessControl = new Mock<IAccessControlService>();
    var handler = new ProductionLicenseHandler(mockAccessControl.Object);
    var license = new License { MemberId = Guid.NewGuid(), ApplicationInstanceId = Guid.NewGuid() };

    // Act
    await handler.Add(license);

    // Assert
    mockAccessControl.Verify(x => x.GrantAccess(license.MemberId, license.ApplicationInstanceId), Times.Once);
}

6. Performance Considerations

For high-volume scenarios, consider batching and caching:

public class OptimizedLicenseHandler : LicenseAdded
{
    private readonly IMemoryCache _cache;
    private readonly IAccessControlService _accessControl;

    public override async Task Add(License license)
    {
        // Check cache before making external calls
var cacheKey = $"license_{license.MemberId}_{license.ApplicationInstanceId}";
        
        if (!_cache.TryGetValue(cacheKey, out _))
     {
            await _accessControl.GrantAccess(license.MemberId, license.ApplicationInstanceId);
     _cache.Set(cacheKey, true, TimeSpan.FromMinutes(5));
        }
  }
}

Troubleshooting

Events Not Being Processed

Problem: Your handler is not being invoked.

Solutions:

  1. Verify the marker class implements IEventHandlerLoader
  2. Ensure AddTenantEventHandlers<T>() is called in ConfigureServices
  3. Check that your handler class is not abstract
  4. Verify the handler is in the same assembly as the marker class
  5. Check Application Insights for exceptions in ICommonEventHandler.OnEvent

Duplicate Event Processing

Problem: Events are processed multiple times.

Solutions:

  1. Make your handlers idempotent
  2. Check for multiple Service Bus subscriptions
  3. Ensure you're not registering handlers multiple times
  4. Verify message lock duration is appropriate

Deserialization Errors

Problem: Events fail to deserialize.

Solutions:

  1. Check that the event payload matches the expected model
  2. Verify JSON property names match (check [JsonProperty] attributes)
  3. Enable detailed logging to see the raw event payload
  4. Check for version mismatches in Veracity.Common.MessagingPack

Handler Dependencies Not Resolved

Problem: Constructor injection fails.

Solutions:

  1. Ensure all dependencies are registered in DI container
  2. Check service lifetimes (handlers are transient, dependencies can be any lifetime)
  3. Verify interface and implementation types match
  4. Check for circular dependencies

Unknown Event Types

Problem: Receiving events for unhandled event types.

Solutions:

  1. Implement handlers for all events your application subscribes to
  2. Set CommonEventHandler.ThrowExceptsionOnUnknownEvents = false to ignore
  3. Review your Service Bus topic filters/subscriptions
  4. Check documentation for new event types

Performance Issues

Problem: Event processing is slow.

Solutions:

  1. Profile your handler code to identify bottlenecks
  2. Consider async operations and parallel processing where appropriate
  3. Implement caching for frequently accessed data
  4. Use batch operations when possible
  5. Review Service Bus settings (concurrent calls, prefetch count)

Testing Locally

To test your handlers locally:

// Create a test event
var testLicense = new License
{
    MemberId = Guid.NewGuid(),
    ApplicationInstanceId = Guid.NewGuid(),
    AccessLevel = "admin",
    SubscriptionState = "Subscribing"
};

var domainEvent = new DomainEvent
{
    EventType = "com.veracity.service.subscription.add",
    EntityType = EntityTypes.Tenant,
    Payload = JsonConvert.SerializeObject(testLicense),
    MessageId = Guid.NewGuid().ToString()
};

// Invoke handler directly
var handler = serviceProvider.GetRequiredService<MyLicenseAddedHandler>();
await handler.Add(testLicense);

// Or via the event dispatcher
var eventHandler = serviceProvider.GetRequiredService<ICommonEventHandler>();
await eventHandler.OnEvent(domainEvent);

Support and Resources

License

This package is licensed under the Apache 2.0 License. See the LICENSE file for details.

Contributing

Contributions are welcome! Please contact the Veracity team for contribution guidelines.


Copyright � Veracity by DNV 2024

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-preview.4 40 1/27/2026
1.0.0-preview.2 40 1/20/2026