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?
9 Answer(s)
-
0
- Goal: When a new
IdentityUseris created in your AuthServer (separate/tiered), automatically create a 1-to-1Personrow 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
IdentityUserby 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
Persontable, create a distributed event handler that listens for the entity created event and inserts aPersonrecord.Conceptually:
- Subscribe to the distributed “entity created” event for
IdentityUserEto - Create
PersonwithUserId = 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
Personin the same DB transaction as user creation across services. Distributed events are asynchronous; design the handler to be idempotent (as shown). - If
Personneeds “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) - Goal: When a new
-
0
Yes, you can place the handler in
.Domain, if your.Domainproject references the ABP Event Bus packages/modules you need (becauseIDistributedEventHandler<T>is an infrastructure concern). Practically, many teams put distributed event handlers in.Application(or a dedicated*.EventHandlersproject) to keep the Domain layer pure, but ABP does not technically prevent you from implementing event handlers in.Domainas 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 methodvirtualso interception works (otherwise you may hit “DbContext can only be created inside a unit of work”).
- Implement
Delete
PersonwhenIdentityUseris deleted (distributed entity event)Handle the distributed entity deleted event and delete the corresponding
Personrow byUserId.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
Personis soft-deletable (implementsISoftDelete), you may preferDeleteAsync(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) -
0
Hi,
Quick correction first —
IdentityUserEtodoes not exist. The earlier AI answer was wrong. What you actually have in ABP Commercial isVolo.Abp.Identity.IdentityUserCreatedEto(inVolo.Abp.Identity.Pro.Application.Contracts). For the delete side, ABP Commercial does not ship a matchingIdentityUserDeletedEto, so you use the framework-levelEntityDeletedEto<UserEto>fromVolo.Abp.Users.Also you don't need to register
EtoMappings.Add<IdentityUser, ...>()—AbpIdentityDomainModulealready does that and turns on auto-event publishing for you.EF Core 1:1 mapping —
Person.Idshould be the user'sIdFor a real 1-to-1 with
IdentityUserin EF Core, the cleanest approach is a PK-to-PK relationship (shared primary key):Person.Idis both the primary key and the foreign key toIdentityUser.Id. No separateUserIdcolumn, 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
MyProjectNameDbContextis already declared as[ReplaceDbContext(typeof(IIdentityDbContext))], implementsIIdentityDbContext, and callsbuilder.ConfigureIdentity()— soIdentityUseris 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(theIdentityUser.Id) directly as thePerson's primary key — noIGuidGeneratorneeded.1) Create the
Personwhen anIdentityUseris createdusing 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); } }IdentityUserCreatedEtocarriesId,TenantIdand aPropertiesdictionary (SendConfirmationEmail,AppName). If you needUserName/Email/Nameon thePerson, fetch the user inside the handler viaIIdentityUserRepositoryorIdentityUserManager.2) Delete the
Personwhen theIdentityUseris deletedA note before the code:
IdentityUseris soft-deleted by ABP (it inheritsFullAuditedAggregateRoot<Guid>which implementsISoftDelete), so deleting a user only setsIsDeleted = true— the row stays in the database. That means a database-levelON DELETE CASCADEon the FK does not fire and you cannot rely on EF Core's cascade behavior to removePerson. 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 anyIdentityUserdeletion path, so no extra wiring is needed on the publisher side. If yourPersonalso implementsISoftDelete,DeleteAsyncwill respect that and only flag it as deleted; otherwise it removes the row.Important caveat about
IdentityUserCreatedEtoIt 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.CreateAsynccall (e.g. custom registration logic) - external login first-time user creation (Google / Microsoft / etc.)
- data seeders / migration code
If you also need
Personin those cases, swap the create handler toIDistributedEventHandler<EntityCreatedEto<UserEto>>— that one is auto-published for everyIdentityUserinsert path:public class PersonCreator : IDistributedEventHandler<EntityCreatedEto<UserEto>>, ITransientDependency { // ... same body, use eventData.Entity.Id }Put the handlers in your
.Applicationproject (it already referencesVolo.Abp.Identity.Pro.Application.ContractsforIdentityUserCreatedEto).Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - a direct
-
0
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) -
0
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 emitsWithOne(), so neither the PK-to-PK shape nor a "separate FK column withWithOne()" 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:
Hand-write
Personwith 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 practicePersonis a satellite ofIdentityUserpopulated 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.Drop the strict 1:1 and use a separate
UserIdFK 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()forPerson.Id. Pick this only if you really want Suite to generate the full CRUD page forPerson.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)