Activities of "Sturla"

Hi, I have tried to read everything on abp.io multi-tenancy (github/discussions/formums) but I´m unsure what route I should take and would like some guidance on how to start the project the correct way.

Scenario: "We give 3party entities access to use our portal and they sell their material back to others (clients)"

  • We offer access to entities (companies/teams/etc) to our portal/framework for a fee (and cut per usage)
  • Entities use the portal for their own material and sell access to it
  • Entities have different users/roles (admin/bookkeeping/agenda/etc)
  • Entities need to be able to do some basic CRM stuff and have access to usage and monetary reports
  • Entities can manage their "subdomain" www.entity.ourPortal.com (or www.ourPortal.com/entity)
  • Clients (random people) can sign-up and pay for access to entities material
  • Logged in client can access material from every entity and pay/subscribe to it.
  • Clients should not need to explicitly choose tenants when logging in. They should be able to login once and browse (and pay for) all material from other tenants
  • We have system-wide settings (look and feel, blog, reports, disable tenants, etc)
  • We distribute fees (money) back to entities based on amount of usage by clients

So my question is basically what architecture approach should we choose based on our requirements and abp.io capabilities? What is possible and/or best approach and/or do you have examples (that attempt to do something similar) for us?

I hope I made myself clear enough, and what my requirements are. If not just shoot back.

  • ABP Framework version: v4.2.2 (or newer since I havent started)
  • UI type: Blazor
  • DB provider: EF Core
  • Identity Server Separated: yes

Thank you so much for this great answer! It has really helped me to scope the work that lies ahead and the tech I should choose. I will explore the path you suggest and do some MVP stuff next couple of days.

After that, I'll mark this question answered and let you know what I decided to do.

Hi

I'm getting CS0104 ambiguous reference errors when adding entities in Suite. Unique namespaces don´t seem to be enough.

Here are two examples of this

1.

First I had this one

C:\Dev\Test\aspnet-core\src\Test.EntityFrameworkCore\EntityFrameworkCore\TestEntityFrameworkCoreModule.cs(55,49): error CS0104: 'EfCoreLanguageRepository' is an ambiguous reference between 'Test.Languages.EfCoreLanguageRepository' and 'Volo.Abp.LanguageManagement.EntityFrameworkCore.EfCoreLanguageRepository' [C:\Dev\Test\aspnet-core\src\Test.EntityFrameworkCore\Test.EntityFrameworkCore.csproj]

For now, I´ll call it "OurLanguages" (or something.. naming is hard) but I would prefer it just being Language and the DB table AppLanguage.

2.

And now I just had this one that I find even stranger

error CS0104: 'TimeZone' is an ambiguous reference between 'Test.TimeZones.TimeZone' and 'System.TimeZone'

Why

Your ABP Framework version. 4.2.2 Your database provider: EF Core Blazor

Yes I'm still in my test phase. I'm aiming on using version 4.3 so hopefully it will be ready when I´m :-)

I was wondering if there was a good way to delete related records, just like you can get all the records with WithDetailsAsync()?

Or will I just have to add all the services (of the related entities I want to delte) to the main one (channelAppService in my example) and iterate through them and manually delete each one (see example 1)?

        /* code in my ChannelAppService */
        
        public async Task<ChannelDto> GetWithDetailsAsync(Guid id)
        {
            //Get the related records General and Settings from _channelRepository
            var queryable = await _channelRepository.WithDetailsAsync(x => x.General, x => x.Settings);

            //Where Channel is == id
            var query = queryable.Where(x => x.Id == id);

            //Get Channel with General and Settings back
            return ObjectMapper.Map<Channel, ChannelDto>(await AsyncExecuter.FirstOrDefaultAsync(query));
        }
        
        // What would be the recomended way to delete the related records of channel?
        public async Task DeleteWithDetailsAsync(Guid id)
        {
            // 1. example
            // But what if the first delete works but the rest fail? 
            _channelRepository.DeleteAsync(id);
            _generalRepository.DeleteAsync(theGeneralId);
            _settingsRepository.DeleteAsync(theSettingsId);
            
            // 2. example
            //OR is there simpler way like this maybe (with some more code obviously) ?
            await AsyncExecuter.DeleteWithDetails(channel);
        }     

This must be fairly common to do so isn't there some examples you can share with me?

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

Ok I think I am somewhere near the answer with .OnDelete(DeleteBehavior.Cascade) but for some reason calling await ChannelAppService.DeleteAsync(id); just marks the Channel entity as deleted (IsDeleted == true) but General doesn't get deleted..

builder.Entity<Channel>(b =>
            {
                b.ToTable("Channel");
                b.ConfigureByConvention();
                
                //Define the relation
                b.HasOne(x => x.General)
                    .WithOne(x => x.Channel)
                    .OnDelete(DeleteBehavior.Cascade)
                    .IsRequired();
                
            });

            builder.Entity<General>(b =>
            {
                b.ToTable("Generals");
                b.ConfigureByConvention();
            });

I'm just calling this from a Blazor page

 await ChannelAppService.DeleteAsync(id);

I know this is likely just EF stuff but since I have wasted 1 question I might as well get the help after all :-)

Thak you! Worked like a charm

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

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 :-)

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 :-)

Showing 1 to 10 of 203 entries
Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11