Veracity.Common.MessagingPack.Tenant
1.0.0-preview.4
Prefix Reserved
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
<PackageReference Include="Veracity.Common.MessagingPack.Tenant" Version="1.0.0-preview.4" />
<PackageVersion Include="Veracity.Common.MessagingPack.Tenant" Version="1.0.0-preview.4" />
<PackageReference Include="Veracity.Common.MessagingPack.Tenant" />
paket add Veracity.Common.MessagingPack.Tenant --version 1.0.0-preview.4
#r "nuget: Veracity.Common.MessagingPack.Tenant, 1.0.0-preview.4"
#:package Veracity.Common.MessagingPack.Tenant@1.0.0-preview.4
#addin nuget:?package=Veracity.Common.MessagingPack.Tenant&version=1.0.0-preview.4&prerelease
#tool nuget:?package=Veracity.Common.MessagingPack.Tenant&version=1.0.0-preview.4&prerelease
Veracity.Common.MessagingPack.Tenant
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
- Installation
- Quick Start
- Core Concepts
- Available Event Handlers
- Event Models
- Configuration
- Usage Examples
- Best Practices
- Troubleshooting
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
- Event Reception: Your Azure Function receives a Service Bus message
- Event Parsing: The
ICommonEventHandlerdeserializes the message into aDomainEvent - Handler Resolution: The framework finds the appropriate handler based on the event type
- Event Processing: Your handler's method is invoked with the strongly-typed payload
- 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 updatedcom.veracity.tenantservice.add- Application installedcom.veracity.service.subscription.add- License addedcom.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 registryOrderNumber- Purchase order numberPricingTier- Pricing tier for the applicationNumberOfLicenses- Number of licenses (null = unlimited)TenantId(inherited) - The tenant where the update appliesName(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 IDApplicationId- Your application's service IDApplicationInstanceId- The tenant-specific installation IDAccessLevel- The assigned access level/roleSubscriptionState- 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 IDElementExternalId- Your application's element identifierElementName- 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:
- Verify the marker class implements
IEventHandlerLoader - Ensure
AddTenantEventHandlers<T>()is called inConfigureServices - Check that your handler class is not abstract
- Verify the handler is in the same assembly as the marker class
- Check Application Insights for exceptions in
ICommonEventHandler.OnEvent
Duplicate Event Processing
Problem: Events are processed multiple times.
Solutions:
- Make your handlers idempotent
- Check for multiple Service Bus subscriptions
- Ensure you're not registering handlers multiple times
- Verify message lock duration is appropriate
Deserialization Errors
Problem: Events fail to deserialize.
Solutions:
- Check that the event payload matches the expected model
- Verify JSON property names match (check
[JsonProperty]attributes) - Enable detailed logging to see the raw event payload
- Check for version mismatches in Veracity.Common.MessagingPack
Handler Dependencies Not Resolved
Problem: Constructor injection fails.
Solutions:
- Ensure all dependencies are registered in DI container
- Check service lifetimes (handlers are transient, dependencies can be any lifetime)
- Verify interface and implementation types match
- Check for circular dependencies
Unknown Event Types
Problem: Receiving events for unhandled event types.
Solutions:
- Implement handlers for all events your application subscribes to
- Set
CommonEventHandler.ThrowExceptsionOnUnknownEvents = falseto ignore - Review your Service Bus topic filters/subscriptions
- Check documentation for new event types
Performance Issues
Problem: Event processing is slow.
Solutions:
- Profile your handler code to identify bottlenecks
- Consider async operations and parallel processing where appropriate
- Implement caching for frequently accessed data
- Use batch operations when possible
- 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
- Developer Portal: https://developer.veracity.com
- API Documentation: https://developer.veracity.com/docs
- Support: Contact Veracity support through the developer portal
- GitHub: Veracity Repositories
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 | Versions 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. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Stardust.Particles (>= 5.0.2)
- System.Memory.Data (>= 8.0.1)
- System.Text.Json (>= 8.0.5)
- Veracity.Common.MessagingPack (>= 1.2.1)
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 |