Starts in:
2 DAYS
14 HRS
28 MIN
4 SEC
Starts in:
2 D
14 H
28 M
4 S

Activities of "jkrause"

maliming,

I messed up and totally forgot to edit the NuGet.config to remove a certain detail from the feed.

Apologies for this oversight, would it be possible/required to invalidate that key and obtain a new one?

Sorry.

I have a simple project, that is roughly the same size in ZIP as our actual project, but whatever.

You can find the source here: Deleted

The application is setup to use LocalDb 2019 (came with VS2022 installer). After giving the default admin user permissions to see the module, click on the Companies menu item and you will get the error. Saving a new Company gives the same error.

Getting just this simple "clean" application to compile and start was frustration, as the generated files did not compile and some ConsoleTest project refused to build, so I removed that from the solution.

(The ctor generated for Company resulted in an illegal order of parameters in combination with nullable types, so I had to fix that manually)

I will try to reproduce this in a "barren" project, but it might very well not happen, which is exactly why I am asking troubleshooting for our actual project. There are no code samples available anywhere about creating a custom module that needs to use the User and Organization.

I'll report back if I can extract all the effort I did, not happy with this answer. Our actual project is not that much "bigger" than the starting template with 1 custom module containing half a dozen entities.

Answer

The duplication also occurs in the ConfigureServices method of the <MyModule>WebModule file:

(Again, I know this is minor and will not actually produce error situations so understandble if this would be of a lesser priority)

Not sure how to create a "simple" project that still reflects the setup and intent that we want to use within our Application. But since the project contains very little IP, I have no issue sharing the solution in its entirety.

Could this be transferred to you in a secure manner?

Hi,

In our application we have the following structure: Main Application -> Our Custom Module -> ABP Module(s). Inside this custom module, we want to be able to link our data with a User and/or Organisation. So through ABP Suite we have setup the needed navigation properties:

This took some effort, and if we didn't have the Commercial source package, I am not sure how to have accomplished this properly from inside a module, but that is besides the issue I am raising. Looking at the provided sample code generated within the AcmeDbContext class, we can see that [ReplaceDbContext(typeof(IIdentityProDbContext))] is used to replace that implementation with that of our own AcmeDbContext class.

Assuming this is how it is done, we did the same in our ModuleDbContext class and added the [ReplaceDbContext(typeof(IIdentityProDbContext))] attribute. We added the required DbSet properties and made sure that builder.ConfigureIdentityPro(); is called. We then added [ReplaceDbContext(typeof(IAcmeDbContext))] attribute to the main application AcmeDbContext so we can also expose our new entities there for later use (a call to builder.ConfigureAcme(); was also added as we replicated that setup).

We have a Company entity that we wish to connect to a User and Organization and using the Suite configuration above it generates the following class:

public partial class Company : FullAuditedEntity<Guid>, IMultiTenant
{
    public Guid OrganizationId { get; set; }    // OrganizationUnit is referenced here as expected
    public Guid UserId { get; set; }            // IdentityUser is referenced here as expected
    public Guid? TenantId { get; set; }

    /* Snip! */
}

Subsequently, the WithNavigationProperties class is generated as expected (putting it here for reference and completeness sake):

public class CompanyWithNavigationProperties
{
    public Company Company { get; set; }
    public OrganizationUnit OrganizationUnit { get; set; }
    public IdentityUser IdentityUser { get; set; }
}

And the necessary methods within the EfCoreCompanyRepository class is also available:

protected virtual async Task<IQueryable<CompanyWithNavigationProperties>> GetQueryForNavigationPropertiesAsync()
{
    return from company in (await GetDbSetAsync())
           join organizationUnit in (await GetDbContextAsync()).OrganizationUnits
               on company.OrganizationId equals organizationUnit.Id into organizationUnits
           from organizationUnit in organizationUnits.DefaultIfEmpty()
           join identityUser in (await GetDbContextAsync()).Users
               on company.UserId equals identityUser.Id into users
           from identityUser in users.DefaultIfEmpty()

           select new CompanyWithNavigationProperties
           {
               Company = company,
               OrganizationUnit = organizationUnit,
               IdentityUser = identityUser
           };
}

We build and start the application and navigate to the 'Companies' menu item; here we are treated with the following:

  1. Error 500 dialog in our Application
  2. Error Value cannot be null. (Parameter 'inner') message inside the EF Core log

After a slight refactoring of this particular method (as I wanted to get more familiar with the generated code and why it looked like that) it looks like the following:

protected virtual async Task<IQueryable<CompanyWithNavigationProperties>> GetQueryForNavigationPropertiesAsync()
{
    // Retrieve the current DbContext once (does it make a difference?)
    var dbContext = await GetDbContextAsync();

    // Add explicit references to the DbSet properties so we can easily inspect them during Debug
    var companies = dbContext.Companies;                // This is (await GetDbSetAsync()) in generated code
    var organizations = dbContext.OrganizationUnits;    // This DbSet<T> is null!
    var identityUsers = dbContext.Users;                // This DbSet<T> is null!

    return from company in companies

           // The generated join is an 'Outer Join' even though our navigations are 'Required', but irrelevant to the issue
           join organizationUnit in organizations
               on company.OrganizationId equals organizationUnit.Id into organizationUnits
           from organizationUnit in organizationUnits.DefaultIfEmpty()

           // The generated join is an 'Outer Join' even though our navigations are 'Required', but irrelevant to the issue
           join identityUser in identityUsers
               on company.UserId equals identityUser.Id into users
           from identityUser in users.DefaultIfEmpty()

           select new CompanyWithNavigationProperties
           {
               Company = company,
               OrganizationUnit = organizationUnit,
               IdentityUser = identityUser
           };
}

Now I could see that the DbSet properties that we took from the IIdentityProDbContext interface are seemingly not properly referring, or mapped to the table on this particular context, even though by my current, and still very much growing understanding of ABP, was configured correctly using [ReplaceDbContext(typeof(IIdentityProDbContext))]?

TL;DR, after using ReplaceDbContext the implemented DbSet properties are null inside a module that the main application is using.

What am I missing here? Why is Users and Organization null? Did I miss some key configuration part to make this work? Did I totally misunderstand the intention here? All the wiring of this logic is happening inside the Module and not the Application. We will make and use different pages and logic there that does not belong to this module. I have also checked the various sources of the Commercial modules, but it seems nothing is making a reference to either a User or Organization.

To summarize:

  1. Module has its own DbContext and entities, but Migration is handled in the Application
  2. Entities have either a required or optional link to an Organization and/or a User
  3. ReplaceDbContext was used inside the Module (IdentityPro) and also the Application (Module, IdentityPro) but the 'inherited' DbSet properties are null
  4. DependsOn was used inside the Module (IdentityPro) and also the Application (IdentityPro and Module)
  5. The AppService, Repository and Pages for the entities (Company) are inside the Module
  6. Additional properties have been added to IdentityUser and OrganizationUnit (that are migrated, displayed and working properly)

Hi bolenton,

(Note: Making some assumptions here about the version of ABP that you are using, so I am going to assume v4.4 or v5.0 for the sake of my answer)

Disclaimer: Though my answer may work for your particular case, please don't view this as an official response from Support nor the proper and/or intended work-around to address this particular issue.

Whenever you create a new Tenant, there is an event handler class that is fired inside your project/module: AcmeTenantDatabaseMigrationHandler (where Acme is your project/module name).

Inside this class there is a method sequence that is being called whenever a new Tenant is created:

public async Task HandleEventAsync(TenantCreatedEto eventData)

This is called when you create a Tenant from inside the SaaS management screens. Inside it makes a call to the MigrateAndSeedForTenantAsync method that does a few things:

  1. Try to apply migrations to the target database in case the Tenant used a dedicated database
  2. Seed the default admin user to the newly created Tenant

There seems to be however a small issue, though perhaps unrelated, when [v] Use Shared Database is used, the code should not try to find an associated connection string for the newly created Tenant. When it cannot find the connection string, it tries to resolve an invalid database connection, which subsequently times out.

Though personally not sure how it fixes it, inside the MigrateAndSeedForTenantAsync method you can see the usage of the Unit of Work that is responsible for commiting your Tenant admin user:

// Seed data
using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true))
{
    await _dataSeeder.SeedAsync(
        new DataSeedContext(tenantId)
            .WithProperty(IdentityDataSeedContributor.AdminEmailPropertyName, adminEmail)
            .WithProperty(IdentityDataSeedContributor.AdminPasswordPropertyName, adminPassword)
    );

    await uow.CompleteAsync();
}

Here you can see that requiresNew: true is being used, which if I understand it correctly, launches a new Unit of Work, which then cannot resolve the connection string of the current Tenant, and failes (due to time out). By settings this value to false, the "current" connection string of the existing Unit of Work is used, and thus the admin user is created as expected.

using (var uow = _unitOfWorkManager.Begin(requiresNew: false, isTransactional: true))

Hope this helps.

Answer

@albert,

The same thing happens for the <MyModule>ApplicationAutoMapperProfile class. I am regenerating the code, since I made changes to the templates, and the mapping code is then duplicated.

Situation after first code generation:

CreateMap<OrganizationUnit, LookupDto<Guid>>().ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.DisplayName));
CreateMap<IdentityUser, LookupDto<Guid?>>().ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.Email));

Method contents after second code generation:

CreateMap<OrganizationUnit, LookupDto<Guid>>().ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.DisplayName));
CreateMap<IdentityUser, LookupDto<Guid?>>().ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.Email));

// This is duplicated
CreateMap<IdentityUser, LookupDto<Guid>>().ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.Email));

As you can observe, it seems that the CRUD generator does not "see" that this mapping already exists? Similarly, this also happens with other mapping statements, where you would end up with multiple definitions when you regenerate the code for multiple entities in succession (still well within regular usage of the CRUD tooling I would say):

CreateMap<Company, CompanyDto>();
CreateMap<CompanyWithNavigationProperties, CompanyWithNavigationPropertiesDto>();

CreateMap<Company, LookupDto<Guid?>>().ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.Name));
// This is duplicated
CreateMap<Company, CompanyDto>();

Edit: The namespace duplication happens a lot as well for the *AppService classes and the classes *WithNavigationPropertiesDto.

(though I do realize this is minor and will not actually produce error situations)

Hope this explains it well enough, there are other parts where the Regex seems to be too greedy and will match too much code, even code the CRUD generator did not create, but I moved that code out to separate files to avoid it all to gether.

Thanks again for your time and effort.

Answer

ABP: v5.0.0-rc.1 CLI: v5.0.0-rc.1 Suite: v5.0.0-rc.1


When (re)generating the the various files using the CRUD generator the <MyModule>EntityFrameworkCoreModule class will:

  • Have the namespaces repeated on the top, e.g.:
using Acme.Abp.Crm.Contacts;
using Acme.Abp.Crm.Companies;
using Acme.Abp.Crm.Companies;
using Acme.Abp.Crm.Companies;
using Acme.Abp.Crm.Contacts;
using Acme.Abp.Crm.Companies;
  • Have the AddRepository mapping repeated inside ConfigureServices, e.g.:
options.AddRepository<Company, Companies.EfCoreCompanyRepository>();
options.AddRepository<Contact, Contacts.EfCoreContactRepository>();
options.AddRepository<Company, Companies.EfCoreCompanyRepository>();
options.AddRepository<Company, Companies.EfCoreCompanyRepository>();
options.AddRepository<Company, Companies.EfCoreCompanyRepository>();
options.AddRepository<Contact, Contacts.EfCoreContactRepository>();

Edit: This will happen after an Entity is updated (more properties, extra navigations) and then Save and generate is clicked, or when you simply click it twice. When you then switch to another entity that gets updated or otherwise and click Save and generate then this one will be repeated as well.

I would like to create a computed property for the purpose of using this in the navigation dropdown as the display property. I think this scenario could be pretty common, especially with the desire to often add the IdentityUser as a navigation property to one of your own entities. So for that purpose I wanted to add a "FullName" property to IdentityUserDto, instead of now only being able to choose between Name and Surname that is available on this type (other properties are of course available, but not relevant to the question).

Having already been successful in adding multiple 'regular' properties to the IdentityUser entity and the IdentityUserDto class, I am thus familiar with the various options, and also what is currently available as information from the documentation. However, how could we achieve this type of extension property? I am unable to figure out how to access the Dto instance itself when providing the configuration, so I am confused how I can create my property.

As a reference, this is how I would declare the property if the Dto class was my own:

public string FullName => string.Join(" ", string.Empty, Name, Surname).Trim();

or as a common variation using interpolation:

public string FullName => $"{Name} {Surname}).Trim();

What do I need to provide to the configuration parameter to achieve the equivalent result?

user.AddOrUpdateProperty<string>(UserConsts.FullName, options => options.DefaultValue = <What should I use here?>);

I also looked at the DefaultValueFactory but this Func does not provide access to the referencing type.

Note: If this is currently not possible through the regular extension methods, is there any alternative or other work-around to achieve something similar?

Thanks for your time and effort.

Showing 11 to 20 of 24 entries
Made with ❤️ on ABP v9.1.0-preview. Updated on November 20, 2024, 13:06