0

I'm trying to associate a one to many relationship between a user and a class. When i try to create a Post I'm also trying to add it to the user-model, but I can't seem to get it right.

User which should be able to have multiple project enteties

public class AppUser : IdentityUser
{
    public ICollection<UserProject> Projects { get; set; }
}

Project model

public class UserProject
{
    public int Id { get; set; }

    public string Name { get; set; }
}

The action to add a project and associate it with a user

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(UserProject userProject)
    {

        if (ModelState.IsValid)
        {
            AppUser appUser = await userManager.GetUserAsync(HttpContext.User);
            appUser.Projects.Add(userProject);

            context.Projects.Add(userProject);
            await context.SaveChangesAsync();

            return RedirectToAction("Index");
        }
        return View(userProject);
    }

However this context.Projects.Add(userProject); results in the error NullReferenceException: Object reference not set to an instance of an object. Would someone please tell me what's wrong and how to achive what I'm trying to do?

DB Context

public class ScrumApplicationContext : IdentityDbContext<AppUser>
{
    public ScrumApplicationContext(DbContextOptions<ScrumApplicationContext> options)
        : base(options)
    {
    }
    public DbSet<UserProject> Projects { get; set; }

}

Startup configureservices

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        services.AddDbContext<ScrumApplicationContext>(options => options.UseSqlServer(Configuration.GetConnectionString("ScrumApplicationContext")));

        services.AddIdentity<AppUser, IdentityRole>()
            .AddEntityFrameworkStores<ScrumApplicationContext>()
            .AddDefaultTokenProviders();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "areas",
                pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
            );

            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}"
            );
        });
        CreateAdminRole(serviceProvider);
        CreateAdminUserAsync(serviceProvider);
    }

Create view

@model ScrumApp.Models.UserProject

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>

            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>

            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
WilliamG
  • 301
  • 1
  • 4
  • 15
  • 1
    How do you instantiate context? Do you inject in to the constructor? If so, do you set it up correctly in Startup class, with AddDbConext? – tdragon Jan 12 '20 at 17:29
  • Yes I inject the context in the constructor and i think i have set up everything correctly, I will edit the post and view dbContext class and configureservices method. – WilliamG Jan 12 '20 at 17:33
  • 2
    1. Could you please show us your startup? 2. When the action method `public async Task Create(UserProject userProject)` is executed, is `userProject` is null ? Could you please show us the code of your View (Form)? 3. When this line "AppUser appUser = await userManager.GetUserAsync(HttpContext.User);" is executed, is the `appUser` a expected result? – itminus Jan 13 '20 at 01:52
  • I have added the asked for files and also for question 3, yes th appUser is the expected result. The project itself does create, but it does't get added in the users projects collection. – WilliamG Jan 13 '20 at 08:37
  • is `userProject` null or `context.Projects` null? or `context`? – Konrad Jan 13 '20 at 08:49
  • also see https://stackoverflow.com/questions/48623533/how-to-load-navigation-properties-on-an-identityuser-with-usermanager – Konrad Jan 13 '20 at 08:56

2 Answers2

1

Try to add foreign keys to User and Project to the UserProject class.

public class UserProject
{
    public int Id { get; set; }

    public string Name { get; set; }

    public int UserId { get; set; }
    public int ProjectId { get; set; }

    [ForeignKey("UserId")]
    public User User { get; set; }

    [ForeignKey("ProjectId")]
    public Project Project { get; set; }
}

Then you could add the entities:

var userProject = new UserProject { UserId=.., ProjectId=.. };
context.UserProjects.Add(userProject);
Martin Staufcik
  • 5,801
  • 4
  • 32
  • 46
0

The issue is that Projects is initially null. You need to either initialize it first:

appUser.Projects ??= new List<UserProject>();
appUser.Projects.Add(userProject);

Or simply set a default on the property:

public ICollection<UserProject> Projects { get; set; } = new List<UserProject>();

Having at least one project (and including the relationship in the query) works around the issue as EF will have instantiated the collection already. However, that doesn't solve the problem in all scenarios and is not a "solution". You need to plan for and handle null appropriately.

Chris Pratt
  • 207,690
  • 31
  • 326
  • 382