Open Closed

Using Distributed Event Bus To Automatically Populate a Person Table When Identity User Is Created #10646


User avatar
0
punta created

My Person entity extends Identity User entity by 1-to-1 relationship. The .AuthServer project is separate with a tiered architecture. How to make use of Distributed Event Bus to create the Person table row automatically when Identity User is created?

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

9 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Goal: When a new IdentityUser is created in your AuthServer (separate/tiered), automatically create a 1-to-1 Person row in your main application using ABP Distributed Event Bus.

    1) Publish a distributed “entity created” event for IdentityUser (in AuthServer)

    ABP can automatically publish distributed events for entity create/update/delete once you configure it.

    In your AuthServer module (where EF Core + Identity runs), configure distributed entity events for IdentityUser by registering an Event Transfer Object (ETO) for it (so it can be sent over the distributed bus):

    using Microsoft.Extensions.DependencyInjection;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Abp.Identity;
    
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpDistributedEntityEventOptions>(options =>
        {
            options.EtoMappings.Add<IdentityUser, IdentityUserEto>();
            // IdentityUserEto exists in ABP and is used for transferring IdentityUser over distributed bus.
        });
    }
    

    Once this is configured, when a user is created in AuthServer (e.g., via IdentityUserAppService / IdentityUserManager), ABP will publish a distributed entity-created event for that entity.

    2) Handle the event in the “Person” owning service/module (your main app)

    In the service that owns the Person table, create a distributed event handler that listens for the entity created event and inserts a Person record.

    Conceptually:

    • Subscribe to the distributed “entity created” event for IdentityUserEto
    • Create Person with UserId = eventData.Entity.Id (and any other fields you want)

    Example shape:

    using System;
    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Repositories;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Abp.EventBus.Entities;
    using Volo.Abp.Identity;
    
    public class IdentityUserCreated_CreatePersonHandler :
        IDistributedEventHandler<EntityCreatedEto<IdentityUserEto>>,
        ITransientDependency
    {
        private readonly IRepository<Person, Guid> _personRepository;
    
        public IdentityUserCreated_CreatePersonHandler(IRepository<Person, Guid> personRepository)
        {
            _personRepository = personRepository;
        }
    
        public async Task HandleEventAsync(EntityCreatedEto<IdentityUserEto> eventData)
        {
            var user = eventData.Entity;
    
            // Idempotency: ensure you don’t insert duplicates (important for distributed systems)
            var existing = await _personRepository.FirstOrDefaultAsync(x => x.UserId == user.Id);
            if (existing != null)
            {
                return;
            }
    
            await _personRepository.InsertAsync(
                new Person(id: GuidGenerator.Create())
                {
                    UserId = user.Id,
                    // populate other fields if needed (Name/Surname/Email etc. are available on the ETO if mapped)
                },
                autoSave: true
            );
        }
    }
    

    3) Ensure both sides are actually using Distributed Event Bus transport

    Make sure:

    • AuthServer publishes to the configured provider (RabbitMQ/Kafka/etc.)
    • The Person-owning application subscribes to the same provider and has the distributed event bus module configured.

    4) Important notes for a separated AuthServer

    • Do not try to create Person in the same DB transaction as user creation across services. Distributed events are asynchronous; design the handler to be idempotent (as shown).
    • If Person needs “current user” auditing fields (CreatorId, etc.), remember that distributed handlers typically run without the original HTTP user context. If you need to set audit fields based on a specific principal, ABP provides a way to change the current principal in a scope.

    Sources (verified):

    • https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/
    • https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed#event-transfer-object
    • https://abp.io/docs/latest/framework/infrastructure/current-user#changing-the-current-principal

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    punta created

    Can I place the Event Handler directly inside the .Domain project? The Person entity is defined there. Also, How to Delete a Person when an Identity User is deleted?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Yes, you can place the handler in .Domain, if your .Domain project references the ABP Event Bus packages/modules you need (because IDistributedEventHandler<T> is an infrastructure concern). Practically, many teams put distributed event handlers in .Application (or a dedicated *.EventHandlers project) to keep the Domain layer pure, but ABP does not technically prevent you from implementing event handlers in .Domain as long as DI can discover/register them.

    • If you put it in .Domain, ensure it’s registered and runs inside a Unit of Work:

      • Implement IDistributedEventHandler<...>
      • Add ITransientDependency
      • If you use [UnitOfWork], make the handler method virtual so interception works (otherwise you may hit “DbContext can only be created inside a unit of work”).

    Delete Person when IdentityUser is deleted (distributed entity event)

    Handle the distributed entity deleted event and delete the corresponding Person row by UserId.

    using System;
    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Repositories;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Abp.EventBus.Entities;
    using Volo.Abp.Identity;
    using Volo.Abp.Uow;
    
    public class IdentityUserDeleted_DeletePersonHandler :
        IDistributedEventHandler<EntityDeletedEto<IdentityUserEto>>,
        ITransientDependency
    {
        private readonly IRepository<Person, Guid> _personRepository;
    
        public IdentityUserDeleted_DeletePersonHandler(IRepository<Person, Guid> personRepository)
        {
            _personRepository = personRepository;
        }
    
        [UnitOfWork]
        public virtual async Task HandleEventAsync(EntityDeletedEto<IdentityUserEto> eventData)
        {
            var userId = eventData.Entity.Id;
    
            var person = await _personRepository.FirstOrDefaultAsync(x => x.UserId == userId);
            if (person == null)
            {
                return; // idempotent
            }
    
            await _personRepository.DeleteAsync(person, autoSave: true);
        }
    }
    

    (Recommended) also handle “soft delete” vs “hard delete”

    If your Person is soft-deletable (implements ISoftDelete), you may prefer DeleteAsync (it will soft-delete). If you want to hard delete regardless, you’d need to bypass soft-delete filters/behavior (implementation depends on your setup), but the above is the standard ABP approach.

    Reliability note (important in separated/tiered)

    Because this is cross-service, you should enable/consider Outbox/Inbox so user deletion + event publishing and event consumption are reliable and transactional on each side.

    Links:

    • https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/
    • https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events
    • https://abp.io/docs/latest/framework/architecture/domain-driven-design/domain-services

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    punta created

    IdentityUserEto is not defined. How to make it in context? Does it exist in Modern ABP?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Quick correction first — IdentityUserEto does not exist. The earlier AI answer was wrong. What you actually have in ABP Commercial is Volo.Abp.Identity.IdentityUserCreatedEto (in Volo.Abp.Identity.Pro.Application.Contracts). For the delete side, ABP Commercial does not ship a matching IdentityUserDeletedEto, so you use the framework-level EntityDeletedEto<UserEto> from Volo.Abp.Users.

    Also you don't need to register EtoMappings.Add<IdentityUser, ...>() — AbpIdentityDomainModule already does that and turns on auto-event publishing for you.

    EF Core 1:1 mapping — Person.Id should be the user's Id

    For a real 1-to-1 with IdentityUser in EF Core, the cleanest approach is a PK-to-PK relationship (shared primary key): Person.Id is both the primary key and the foreign key to IdentityUser.Id. No separate UserId column, no extra unique index needed, and the relationship is automatically required.

    public class Person : Entity<Guid>
    {
        public string FullName { get; set; }
    
        protected Person() { }
        public Person(Guid identityUserId) : base(identityUserId) { }
    }
    

    This works directly with the standard ABP application template, because your MyProjectNameDbContext is already declared as [ReplaceDbContext(typeof(IIdentityDbContext))], implements IIdentityDbContext, and calls builder.ConfigureIdentity() — so IdentityUser is part of the same model and the fluent API below resolves the FK target without any extra setup.

    builder.Entity<Person>(b =>
    {
        b.ToTable("Persons");
        b.ConfigureByConvention();
    
        b.HasOne<IdentityUser>()
            .WithOne()
            .HasForeignKey<Person>(p => p.Id);
    });
    

    This means inside the event handlers below, you pass eventData.Id (the IdentityUser.Id) directly as the Person's primary key — no IGuidGenerator needed.

    1) Create the Person when an IdentityUser is created

    using System;
    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Repositories;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Abp.Identity;
    
    public class PersonCreator :
        IDistributedEventHandler<IdentityUserCreatedEto>,
        ITransientDependency
    {
        private readonly IRepository<Person, Guid> _personRepository;
    
        public PersonCreator(IRepository<Person, Guid> personRepository)
        {
            _personRepository = personRepository;
        }
    
        public async Task HandleEventAsync(IdentityUserCreatedEto eventData)
        {
            if (await _personRepository.AnyAsync(x => x.Id == eventData.Id))
            {
                return; // idempotent — important for distributed handlers
            }
    
            await _personRepository.InsertAsync(new Person(eventData.Id), autoSave: true);
        }
    }
    

    IdentityUserCreatedEto carries Id, TenantId and a Properties dictionary (SendConfirmationEmail, AppName). If you need UserName / Email / Name on the Person, fetch the user inside the handler via IIdentityUserRepository or IdentityUserManager.

    2) Delete the Person when the IdentityUser is deleted

    A note before the code: IdentityUser is soft-deleted by ABP (it inherits FullAuditedAggregateRoot<Guid> which implements ISoftDelete), so deleting a user only sets IsDeleted = true — the row stays in the database. That means a database-level ON DELETE CASCADE on the FK does not fire and you cannot rely on EF Core's cascade behavior to remove Person. You really do need an event handler:

    using System;
    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Domain.Entities.Events.Distributed;
    using Volo.Abp.Domain.Repositories;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Abp.Users;
    
    public class PersonRemover :
        IDistributedEventHandler<EntityDeletedEto<UserEto>>,
        ITransientDependency
    {
        private readonly IRepository<Person, Guid> _personRepository;
    
        public PersonRemover(IRepository<Person, Guid> personRepository)
        {
            _personRepository = personRepository;
        }
    
        public async Task HandleEventAsync(EntityDeletedEto<UserEto> eventData)
        {
            var person = await _personRepository.FindAsync(eventData.Entity.Id);
            if (person != null)
            {
                await _personRepository.DeleteAsync(person, autoSave: true);
            }
        }
    }
    

    EntityDeletedEto<UserEto> is auto-published by ABP for any IdentityUser deletion path, so no extra wiring is needed on the publisher side. If your Person also implements ISoftDelete, DeleteAsync will respect that and only flag it as deleted; otherwise it removes the row.

    Important caveat about IdentityUserCreatedEto

    It is only published from IIdentityUserAppService.CreateAsync (the Identity Pro management UI / API). It will NOT fire when a user is created via:

    • a direct IdentityUserManager.CreateAsync call (e.g. custom registration logic)
    • external login first-time user creation (Google / Microsoft / etc.)
    • data seeders / migration code

    If you also need Person in those cases, swap the create handler to IDistributedEventHandler<EntityCreatedEto<UserEto>> — that one is auto-published for every IdentityUser insert path:

    public class PersonCreator :
        IDistributedEventHandler<EntityCreatedEto<UserEto>>,
        ITransientDependency
    {
        // ... same body, use eventData.Entity.Id
    }
    

    Put the handlers in your .Application project (it already references Volo.Abp.Identity.Pro.Application.Contracts for IdentityUserCreatedEto).

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    punta created

    Thanks Maliming Very Much! You saved me a lot.

    One quick question: Is it possible to create an Entity with a PK-to-PK relationship (shared primary key) in ABP Suite? Can I create the Person entity using ABP Suite?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Short answer: Suite (v10.3) can't really help here. Its entity designer doesn't have a 1:1 option at all — it generates 1:N navigation properties (HasOne(...).WithMany().HasForeignKey(...)) and N:N connection tables. There's no template that emits WithOne(), so neither the PK-to-PK shape nor a "separate FK column with WithOne()" 1:1 can be generated by Suite directly. It's also not designed to "scaffold UI on top of an existing hand-written entity" — Suite generates the entity, EF config, application service, controller, and pages together from its own model.

    So your two realistic options are:

    1. Hand-write Person with the PK-to-PK shape from my previous answer. This keeps the relationship a true 1:1, the handler stays clean (new Person(eventData.Id)), and you write any application service / UI yourself if you ever need them. In practice Person is a satellite of IdentityUser populated by the event handler and rarely needs its own admin CRUD page (user management lives in the Identity Pro UI), so this is what I'd recommend.

    2. Drop the strict 1:1 and use a separate UserId FK column with a unique index. This is what Suite can actually scaffold (Domain + Application + Controller + UI), but model-wise it's a 1:N relationship that you "force" into 1:1 with a unique index — not a true EF Core 1:1. You also pay an extra column and have to use _guidGenerator.Create() for Person.Id. Pick this only if you really want Suite to generate the full CRUD page for Person.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    punta created

    Great takeaway! Thanks a lot!

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    👍

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.