Open Closed

Using ReplaceDbContext inside module yields 'null' DbSet properties on the Context when using Navigation properties #2241


User avatar
0
jkrause created

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)

10 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Please share more code. eg DbContext and Your EF Core Module or share a simple project that can reproduce the problem,

    Thanks

  • User Avatar
    0
    jkrause created

    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?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    A complete project will make troubleshooting difficult, you can create a simple project according to your steps.

  • User Avatar
    0
    jkrause created

    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.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Waiting your good news.

  • User Avatar
    0
    jkrause created

    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)

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I will check it, Please share the commercial project via email, Thanks.

  • User Avatar
    0
    jkrause created

    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.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    You can send email to info@abp.io to request to change a API Key.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Add {set;} to you dbset.

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