skip to Main Content

In my .NET 7 Web API, the navigation property on the parent (User) remains null despite the dependent (Researcher) correctly containing the parents id property. I am attempting to create both the User and the Researcher at the same time within the AuthService. Any help as to how I can get the User to contain the reference to the Researcher would be greatly appreciated.

Edit:
After further testing I noticed that User.Researcher and Researcher.User are both null. The only reference I have is that Researcher.UserId is correct.

The API response:

[
  {
    "userId": "d8725e37-b3b2-4851-8ae7-07afadb92adc",
    "email": "[email protected]",
    "passwordHash": "$2a$11$hfvUxb44m.b0/RYWBwyQvuaxF0mcRhmiGmqjldvOlDiAmnOiHF92i",
    "researcher": null
  },
  {
    "userId": "3eb55b33-8c41-405e-a19c-5390c472302e",
    "email": "[email protected]",
    "passwordHash": "$2a$11$uwvZErMttnzYRpwUO2LfOOvvjURZrSs5V68lp2L8.MtC6gubs7bkO",
    "researcher": null
  },
  {
    "userId": "b2fc3e76-02c5-48a4-b865-b686870e429a",
    "email": "[email protected]",
    "passwordHash": "$2a$11$hKDeNsfK5qnPf5uFujKgu.LB5d/FSDVFnj.vN49vAmd.zZn/ogaLW",
    "researcher": null
  }
]

The api schema:

[
  {
    "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "email": "string",
    "passwordHash": "string",
    "researcher": {
      "researcherId": 0,
      "firstName": "string",
      "lastName": "string",
      "field": "string",
      "areaOfFocus": "string",
      "biography": "string",
      "user": "string",
      "joinEntities": [
        {
          "researcherArticleId": 0,
          "researcherId": 0,
          "researcher": "string",
          "articleId": 0,
          "article": {
            "articleId": 0,
            "title": "string",
            "url": "string",
            "articleData": "string",
            "joinEntities": [
              "string"
            ]
          }
        }
      ]
    }
  }
]

User.cs Parent:

    public class User
    {
        public Guid UserId { get; set; } = Guid.NewGuid();
        public string Email { get; set; } = string.Empty;
        public string PasswordHash { get; set; } = string.Empty;
        public Researcher? Researcher { get; set; }
    }

Researcher.cs Dependent:

    public class Researcher
    {
        [Key]
        public int ResearcherId { get; set; }
        public string FirstName { get; set; } = string.Empty;
        public string LastName { get; set; } = string.Empty;
        public string Field { get; set; } = string.Empty;
        public string AreaOfFocus { get; set; } = string.Empty;
        public string Biography { get; set; } = string.Empty;
        [ForeignKey("UserId")]
        public User? User { get; set; }
        public List<ResearcherArticle>? JoinEntities { get; set; }

    }

AuthService.cs method for creating both the User and Researcher in the same call:

public async Task<ServiceResponse<UserRegisterResponseDto>> RegisterUser(UserRegisterRequestDto request)
        {
            List<string> userValidationMessages = ValidateUser(request);

            var serviceResponse = new ServiceResponse<UserRegisterResponseDto>() { };

            if (userValidationMessages.Count > 0)
            {
                serviceResponse.Success = false;
                serviceResponse.Messages = userValidationMessages;
                return serviceResponse;
            }

            string passwordHash
                = BCrypt.Net.BCrypt.HashPassword(request.Password);
            User userToReturn = new()
            {
                Email = request.Email,
                PasswordHash = passwordHash
            };

            Researcher reasercherToAdd = new()
            {
                User = userToReturn,
            };

            _db.Researchers.Add(reasercherToAdd);

            userToReturn.Researcher = reasercherToAdd;
            _db.Users.Add(userToReturn);
            bool userAdded = await _db.SaveChangesAsync() > 0;

            if (!userAdded)
            {
                serviceResponse.Success = false;
                serviceResponse.Messages = new List<string>() { userAdded.ToString() };
                return serviceResponse;
            }

            serviceResponse.Success = true;
            serviceResponse.Messages = new List<string>() { "Registration successful" };
       
            return serviceResponse;
        }

2

Answers


  1. Chosen as BEST ANSWER
    1. Utilize AddJsonOptions in Program.cs. This prevents the infinite loop caused in point 2, where the Researcher's navigation property points back to the User therefore, back to itself:
    builder.Services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
    });
    
    1. Utilize .Include within the API call.
    _db.Users.Include("Researcher").ToListAsync();
    
    1. Not sure if this is required, but I also added the virtual keyword to my models. Here's the user model for an example:

    User.cs:

    public class User
        {
            public Guid UserId { get; set; } = Guid.NewGuid();
            public string Email { get; set; } = string.Empty;
            public string PasswordHash { get; set; } = string.Empty;
            public virtual Researcher? Researcher { get; set; }
        }
    

  2. TLDR: just switch to one-to-many. If you have to ask questions about one-to-one you probably made mistake in using it in firsts place due to lack of experience.

    My spider-sense is tingling that your case is not a 1:1 relation and User will always be parent entity. So just make it 1:* and stop suffering.
    I guess you either want to avoid multiple Researcher per User, which could be done unique FK, or just want convenient two-way navigation properties, which can be done in many ways (eg. NotMapped ICollection.FirstOrDefault).

    So the correct way would be to get rid of one-to-one relationship, because it sucks on SQL level itself (or rather, it doesn’t really exists). ORM’s tend to make it bothersome to use, because its even worse idea when using them than it is when using raw SQL. In EF it usually screws up entity tracking and automatic relations in fun way. Don’t do it if you want to avoid alcoholism.

    Additional problem you’ll encounter will be cyclical reference between two entities. While you can force EF to work with that you’ll get all sort of "fun" problems later on (eg. serialization causing stack overflow or OOM). Oh, and deleting them from DB is sooo muuuch fuuun.

    But to answer your question directly, the way to do it is same in raw SQL as in any ORM +/- some automagic.

    1. Create both entities.
    2. Insert to database (SaveChanges in EF)
    3. Obtain inserted ID (EF does that automagically by default)
    4. Update foreign keys (do it by FK, not Navigation Property, or EF will give you hell)

    Because EF does everything in single pass, you can only get one of two ID’s automatically.

    To do it in single operation you’d need to either manually generate PK on application side instead of relying on SQL sequence (that’s what auto increment is), which is very bad idea usually, or skip ORM altogether and do everything in SQL procedure.

    At which point one typically realizes that 1:1 relation was a bad idea in first place.

    Oh, and have in mind that your setup seems to not be configured for Lazy Loading. Which might bite you in arse if you ever try to abuse that problematic feature.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search