Open Closed

Best practice to create an entity with related ones in one go? #1213


User avatar
0
Sturla created

This is probably a two-fold but related question:

  1. Am I using Suite correctly?
  2. How should I create all (also related entites) the data for a entity?

I have a page with data, that when saved, creates MyEvent. The page contains few things that I like to separate out (not be in the same table) so I can use it later somewhere else. See image at bottom

When I use Suite I create the following entities

  • Common
  • Settings
  • GeoRestriction
  • MyEvent

where CommonId,SettingsId and GeoRestrictionId are navigation properties on MyEvent.

This way I can (in MyEventAppService) use the GetWithNavigationPropertiesAsync() method and get the related entities directly into the service (or GetAsync() if I just want the ids to query and use later).

So far so good?

If so then whats the best practice to create MyEvent with the navigationproperties in one go?

Would it be fine to do it like this in MyEventAppService? Will this use unit-of-work (if one insert fails all fails)?

[Authorize(MyPermissions.MyEvent.Create)]
public virtual async Task<MyEventDto> CreateAsync(MyEventCreateDto input)
{
    var myEvent = ObjectMapper.Map<MyEventCreateDto, MyEvent>(input);
    
    // Insert Common/Settings/GeoRestriction and get their id's
    var commontemp = new Common(GuidGenerator.Create()){ Name = input.Common.Name, etc... }
    var common = await _commonRepository.InsertAsync(commontemp, autoSave: true);
    ...
    ... same for Settings and GeoRestriction 
    
    // then "hook" it together by addin their ids to myEvent and save
    myEvent.CommonId = common.Id; 
    myEvent.SettingsId = settings.Id;
    myEvent.GeoRestrictionId = geoRestriction.Id;
    myEvent.TenantId = CurrentTenant.Id;  
    myEvent = await _myEventRepository.InsertAsync(myEvent, autoSave: true);
    return ObjectMapper.Map<MyEvent, MyEventDto>(myEvent);
}
        

I really feel like I have got this down just by writing the question but would like to know for sure.

  • ABP Framework version: v4.2.2
  • UI type: Blazor
  • DB provider: EF Core
  • Identity Server Separated: yes

6 Answer(s)
  • User Avatar
    0
    Sturla created

    Or would it make more sence to use Domain Service here?

    I can see the value in using the Domain Service if I had some business logic (maybe using specifications) so I would contain my business logic there but not in the AppService. So I still think that I should use it like I did in my question but I would like a confirmation

    [Authorize(MyPermissions.MyEvent.Create)]
    public virtual async Task<MyEventDto> CreateAsync(MyEventCreateDto input)
    {
         var myEvent = ObjectMapper.Map<MyEventCreateDto, MyEvent>(input);
         
         // Call the manager
         myEvent = await myEventManager.CreateAsync(myEvent,common,settings,geoRestriction, autoSave: true);  
         
         myEventManager.Update(myEvent);
         
         return ObjectMapper.Map<MyEvent, MyEventDto>(myEvent);
    }
    
    public class MyEventManager : DomainService
    {
        private readonly IRepository<MyEvent, Guid> myEventRepository;
        private readonly IRepository<Common, Guid> commonRepository;
        private readonly IRepository<Settings , Guid> settingsRepository;
        private readonly IRepository<GeoRestriction , Guid> geoRestrictionRepository;
     
        public IssueManager(IRepository<MyEvent, Guid> myEventRepository, Common, Guid> commonRepository, etc)
        {
            this.myEventRepository = myEventRepository;
            this.commonRepository = commonRepository;
            //etc.
        }
        
        
        public async Task CreateAsync(MyEvent myEvent, Common common, Settings settings, GeoRestriction geo, bool autoSave)
        {       
           var _common = await _commonRepository.InsertAsync(common, autoSave: autoSave);
           var _settings = await _settingsRepository.InsertAsync(settings, autoSave: autoSave);
           var _geo = await _geoRestrictionRepository.InsertAsync(geo, autoSave: autoSave);
        
            myEvent.CommonId = _common.Id; 
            myEvent.SettingsId = _settings.Id;
            myEvent.GeoRestrictionId = _geoRestriction.Id;
            myEvent.TenantId = CurrentTenant.Id;  
        }  
    }
    

    I'm just writing this all down so I can better grasp DDD and hopefully help somebody else when they go down this route so please bear with me :-)

  • User Avatar
    0
    Sturla created

    The code above works but what is the correct approach?

    So after finally getting some code to look at Easy CRM example I see that I´m probably doing this incorrectly.

    Here is what I got from that code.

    This is one-to-one relationship

    Create a MyEvent entity with the desired relations in the Domain

    public class MyEvent : FullAuditedAggregateRoot<Guid>
    {
       public virtual Guid? TenantId { get; set; } 
       public virtual string Name { get; set; } 
       public virtual Guid? CommonId { get; set; } 
       public virtual Common Common { get; set; } 
       public virtual Guid? SettingsId { get; set; } 
       public virtual Settings Settings { get; set; } 
       public virtual Guid? GeoRestrictionId { get; set; } 
       public virtual GeoRestriction GeoRestriction { get; set; }  
       public virtual ICollection<Stuff> Stuff { get; set; }
    }
    

    an just for completness I´ll show one related entity

    public class Common : FullAuditedAggregateRoot<Guid>, IMultiTenant
    {
       public virtual Guid? TenantId { get; set; } 
       public virtual string Address { get; set; } 
    
       public virtual MyEvent MyEvent { get; set; } 
    }
    

    then in Contracts we have these two dto's where one is lighter with only ids and the other extended one with the dto-entities I'm working with

    public class MyEventDto : FullAuditedEntityDto<Guid>
    {
        public virtual string Name { get; set; }
        public Guid CommonId { get; set; }
        public Guid SettingsId { get; set; }
        public Guid GeoRestrictionId { get; set; }
     }
    
    public class MyEventCreateAndUpdateExtendedDto : OrderDto
    {
        public CommonDto Common { get; set; }
        public SettingsDto Settings { get; set; }
        public GeoRestrictionDto GeoRestriction { get; set; }
        public List<StuffDto> Stuffs { get; set; }
    }
    

    I ended up not needing this relationship setup, but I´ll keep it here anyways

    builder.Entity<MyEvent>(b =>
    {
        b.ToTable(MyEventConsts.DbTablePrefix + "MyEvent", MyEventConsts.DbSchema);
        b.ConfigureByConvention();
    
        b.Property(x => x.Name).IsRequired();
        
        //This was not needed (because of EF picked up conventions used) 
        //b.HasOne(x => x.Common).WithOne(x => x.MyEvents).HasForeignKey(x => x.CommonId).IsRequired();
        // b.HasOne(x => x.Settings).WithOne(x => x.MyEvents).HasForeignKey(x => x.SettingsId);
        //b.HasOne(x => x.GeoRestriction).WithOne(x => x.MyEvents).HasForeignKey(x => x.GeoRestrictionId);
        // b.HasMany(x => x.Stuff).WithOne(x => x.MyEvent).HasForeignKey(x => x.MyEventId);
    });
    

    then create MyEvent by call this method in the AppService

    [Authorize(MyPermissions.MyEvents.Create)]
    public virtual async Task<MyEventDto> CreateAsync(MyEventCreateAndUpdateExtendedDto input)
    {
        var newMyEvent = ObjectMapper.Map<MyEventCreateAndUpdateExtendedDto, MyEvent>(input);
        newMyEvent.TenantId = CurrentTenant.Id;
    
        var common = new Common(GuidGenerator.Create()){ Name = input.Name, etc... }
        var settings = new Settings(GuidGenerator.Create()){ /*set properties*/ }
        var geoRestriction = new GeoRestriction(GuidGenerator.Create()){ /*set properties*/}
    
        newMyEvent.Common = common;
        newMyEvent.Settings = settings;
        newMyEvent.GeoRestriction = geoRestriction;
        
        var myEvent = await _MyEventRepository.InsertAsync(newMyEvent);
        await CurrentUnitOfWork.SaveChangesAsync();
        return ObjectMapper.Map<MyEvent, MyEventDto>(myEvent);
    }
    

    And now I can use GetWithNavigationPropertiesAsync(id) to get them all back.

    So could anybody chime in and tell me **what is the best approach **and if I´m on the right track or not?

    Sorry for the long rant.. but if nobody answers me I just need to work this out for my self and learn something and hopefully help the next poor bastard that is trying to get this all to click :-)

  • User Avatar
    0
    alper created
    Support Team Director

    Suite doesn't support n*n relationships. So if your target entity will use some other entities as navigation property like below, then you need to create Author and Publisher entities (you may not want to generate the UIs if you don't manage these from UI). Finally create book entity with Author and Publisher navigation properties.

    Book.cs

    • AuthorId
    • PublisherId
  • User Avatar
    0
    abpVAndy created

    He want to know his code design is good or not or have any other suggetions about his code? Am I right?

    I am also has some confuse about the navigation property.

    I use abp suite to create navigation property, it create additional entity named: xxxWithNavigationProperties And I cannot find any code or description about it xxxWithNavigationProperties. No find in commercial examples: easy-crm and bookstore-angular-mongodb

    Dose it has some docs about xxxWithNavigationProperties?

    thx

  • User Avatar
    0
    Sturla created

    You are correct @abpVAndy.. I wanted validation on my code setup when working with related entities and if my usage of navigation properties were correct. I found the given answer sourly lacking so I gave up asking this question and just kept my code as it is above, correct or not...

    I started out trying to be a DDD/abp.io-framework purist but there some many things to learn there I can´t let it stop me, so I just plow ahead and try to read and re-read the docs.

    At some point when I have got every thing running (my style) I´m thinking of paying for a 1-on-1 code-review and coaching. Thats why my code is now littered with //REVIEW: comments where I need future validation.

    One thing I like to add is that I find the docs missing more complex real-world examples.

    That is why I have been asking lot of these kind of questions so others might gain from them. It would fantastic if they spur somebody on to create some.

    Maybe I can give you some insights into the navigation properties.

    And please excuse me if I'm telling you something you already know! Regradless this might help somebody else.

    They are basically something (foreign keys relationship/association) to connect entities together.

    Lets say you have some XXX entity that has these navigation properies e.g. like this public Guid? SomeRelatedEntityId { get; set; } and you use GetAsync(id) method (on XXX) you will get every property set on XXX but you will only get a guid to SomeRelatedEntity and not is properties.

    If you then wanted to update the values in that entity/table you will have to call GetAsync(someRelatedEntityId) on the SomeRelatedEntityRepository to get the properties you need to update.

    With GetXXXWithNavigationProperties methods you will get XXX with SomeRelatedEntity with all its properies filled out and ready to be displayed/used. You don´t need to call again (note that now you are getting more data and maybe not just what you need).

    And now you enter the crux of my question: In Create/Update methods of XXXEntity, is it considered OK to mix the domain objects by calling insert/update on all the navigation properties like this here below.

    public async Task<MyEventDto> CreateAsync(MyEventCreateAndUpdateDto input)
     {   
        var newMyEvent = new MyEvent();
        
        // Create navigation entities and map input data to their respected properties
        var common = new Common(GuidGenerator.Create()){ Name = input.Name, etc... }
        var settings = new Settings(GuidGenerator.Create()){ /*set properties*/ }
        var geoRestriction = new GeoRestriction(GuidGenerator.Create()){ /*set properties*/}
        
        // Insert everything to their respected repositories
        // First I thought that this was incorrect "mixing" domains,
        // but the CreateAsync is a Service so the domains are not mixing..correct?
        var _common = await _commonRepository.InsertAsync(common, autoSave: autoSave);
        var _settings = await _settingsRepository.InsertAsync(settings, autoSave: autoSave);
        var _geo = await _geoRestrictionRepository.InsertAsync(geo, autoSave: autoSave);
    
        // "Connect" the inserted id's to MyEvent
        newMyEvent.CommonId = _common.Id; 
        newMyEvent.SettingsId = _settings.Id;
        newMyEvent.GeoRestrictionId = _geoRestriction.Id;
        newMyEvent.TenantId = CurrentTenant.Id;  
    
        // Save everything connected.. if anything fails above UnitOfWork will see to it that nothing gets saved.
        var myEvent = await _MyEventRepository.InsertAsync(newMyEvent);
    
        // Returns MyEvent that only has ids. 
        // You call GetXXXWithProperties again in your UI/Client code to get them.
        // Or you could create another CreateAsync method that returns another object with everything,
        // but that ofcourse depends on your use case
        return ObjectMapper.Map<MyEvent, MyEventDto>(myEvent);
    } 
    

    Is this the correct approach? It feels little too manual.. Should I maybe inject ICommonAppService, ISettingsAppService etc. into MyEventService and use their service methods to insert/update (like this example of MyService using the BookAppService)? But then you would loose UnitOfWork correct?

  • User Avatar
    0
    abpVAndy created

    Thanks @Sturla you sharing with me some ideas.

    1, From I understanding, yes, we can mix domian. 2, mixing domain, the mixing code should be in DomainService, like xxxManager, instead of ApplicationService 3, For mixing domian, yes, I think your method is the only way, multiple calling "insert" of different domain. 4, Or use distribute event method.

    I hope ABP Master could give us more detail ideas about it. Mix Domain is common scenario and what options we could select???

    For Navigation property, I see their example code, all use below method, add nativation property in existing class. But ABP Suite create additional entity for it. Not know why it create additional entity???

    public class Order : FullAuditedAggregateRoot<Guid>, IMultiTenant { public virtual ICollection<OrderLine> OrderLines { get; set; } }

    public class OrderLine : FullAuditedEntity<Guid>, IMultiTenant { public virtual Product Product { get; set; } }

Made with ❤️ on ABP v9.1.0-preview. Updated on November 19, 2024, 12:56