DbContext is Null when call in primary constructor EF Core on .NET 8
Asked Answered
S

1

6

I'm using .NET 8, my DbContext is null when call in primary constructor, but when call in normal constructor.

This is my DbContext:

public class DataContext(DbContextOptions<DataContext> options) : DbContext(options)
{
    public DbSet<Student> Students { get; set; }
    public DbSet<Subject> Subjects { get; set; }
    public DbSet<Teacher> Teachers { get; set; }
    public DbTSet<Lesson> Lessons { get; set; }
    public DbSet<Attendance> Attendances { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.AddInboxStateEntity();
        builder.AddOutboxMessageEntity();
        builder.AddOutboxStateEntity();

        builder.Ignore<BaseEntity>();
    }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
    {
        foreach (var entity in ChangeTracker
                     .Entries()
                     .Where(x => x is { Entity: BaseEntity, State: EntityState.Modified })
                     .Select(x => x.Entity)
                     .Cast<BaseEntity>())
            entity.UpdatedAt = DateTime.UtcNow;

        return base.SaveChangesAsync(cancellationToken);
    }
}

My controller:

[ApiController]
[Route("api/[controller]")]
public class SubjectsController(DataContext context, IMapper mapper) : ControllerBase
{
    [HttpGet("{room}")]
    [Authorize]
    [AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
    [AuthorizeForScopes(ScopeKeySection = "AzureAd:Scopes")]
    public async Task<ActionResult<SubjectDto>> GetByRoom(string room)
    {
        var userId = Guid.Parse(User.GetObjectId());
        
        var students = await context.Students.OrderBy(x => x.StudentCode).ToListAsync();

        var student = await context.Students.FirstOrDefaultAsync(x => x.Id == userId);
        
        if (student == null) 
            return NotFound("Student not found");

        var now = DateOnly.FromDateTime(DateTime.UtcNow);
        var subject = await context.Subjects
            .Include(x => x.Students)
            .AsSplitQuery()
            .Where(x => x.Students.Contains(student))
            .ProjectTo<SubjectDto>(mapper.ConfigurationProvider)
            .FirstOrDefaultAsync(x =>
                x.Room.Equals(room, StringComparison.CurrentCultureIgnoreCase)
                && x.DateStart <= now && now <= x.DateEnd
                && !x.IsEnded);
        
        if (subject == null) 
            return NotFound("Subject not found");

        return subject;
    }
}

When I execute:

  • With primary constructor

    With primary constructor

  • With normal constructor

    With normal constructor

I want DbContext to be not null when I call the primary constructor

UPDATED:

My Program.cs:

using ApplicationBase.Extensions;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Polly;
using StudentService.Consumers;
using StudentService.Data;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
        services.AddEndpointsApiExplorer();
builder.Services.AddControllers();
builder.Services.AddIdentityService(builder.Configuration);

builder.Services.AddDbContext<DataContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
);

builder.Services.AddMassTransit(opts =>
{
    opts.AddEntityFrameworkOutbox<DataContext>(opt =>
    {
        opt.QueryDelay = TimeSpan.FromSeconds(10);
        opt.UseSqlServer();
        opt.UseBusOutbox();
    });

    opts.AddConsumersFromNamespaceContaining<StudentAuthConsumer>();

    opts.SetEndpointNameFormatter(new KebabCaseEndpointNameFormatter("student-svc", false));

    opts.UsingRabbitMq((context, cfg) =>
    {
        cfg.UseMessageRetry(r =>
        {
            r.Handle<RabbitMqConnectionException>();
            r.Interval(5, TimeSpan.FromSeconds(10));
        });

        cfg.Host(builder.Configuration["RabbitMQ:Host"], "/", host =>
        {
            host.Username(builder.Configuration.GetValue("RabbitMQ:Username", "guest"));
            host.Password(builder.Configuration.GetValue("RabbitMQ:Password", "guest"));
        });

        cfg.ConfigureEndpoints(context);
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseApplicationIdentity();
app.MapControllers();

var retryPolicy = Policy
    .Handle<Exception>()
    .WaitAndRetry(5, _ => TimeSpan.FromSeconds(10));

retryPolicy.ExecuteAndCapture(() => app.InitDb());

app.Run();

Controller with normal constructor in 2nd image:

using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using StudentService.Data;
using StudentService.DTOs;
using StudentService.Models;

namespace StudentService.Controllers;

[ApiController]
[Route("api/[controller]")]
public class SubjectsController : ControllerBase
{
    private readonly DataContext _context;
    private readonly IMapper _mapper;

    public SubjectsController(DataContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    [HttpGet("{room}")]
    [Authorize]
    [AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
    [AuthorizeForScopes(ScopeKeySection = "AzureAd:Scopes")]
    public async Task<ActionResult<SubjectDto>> GetByRoom(string room)
    {
        var userId = Guid.Parse(User.GetObjectId());

        if (!await _context.Students.AnyAsync(x => x.Id == userId))
            return NotFound("Student not found");

        var dateTimeNow = DateTime.UtcNow;
        var dateNow = DateOnly.FromDateTime(dateTimeNow);

        var subject = await _context.Subjects
            .AsSplitQuery()
            .Include(x => x.Students)
            .Where(x => x.Students.Any(s => s.Id == userId))
            .Include(x => x.Lessons)
            .Where(x => x.Lessons.Any(l => l.StartTime <= dateTimeNow && dateTimeNow <= l.EndTime))
            .ProjectTo<SubjectDto>(_mapper.ConfigurationProvider)
            .FirstOrDefaultAsync(x =>
                x.Room.ToLower().Contains(room.ToLower())
                && x.DateStart <= dateNow && dateNow <= x.DateEnd
                && !x.IsEnded
            );

        if (subject == null) return NotFound("Subject not found");

        return subject;
    }

    [HttpPost]
    [Authorize]
    [AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
    [AuthorizeForScopes(ScopeKeySection = "AzureAd:Scopes")]
    public async Task<ActionResult<SubjectDto>> Create(SubjectCreateDto subjectCreateDto)
    {
        var userId = Guid.Parse(User.GetObjectId());

        if (!await _context.Teachers.AnyAsync(x => x.Id == userId))
            return NotFound("Teacher not found");

        var subject = _mapper.Map<Subject>(subjectCreateDto);
        subject.Students.Add(new Student { Id = userId });

        await _context.Subjects.AddAsync(subject);

        var result = await _context.SaveChangesAsync() > 0;

        if (!result) return BadRequest();

        return _mapper.Map<SubjectDto>(subject);
    }
}

My packages:

  • Main service: Main service
  • Base service: Base service
Sopping answered 10/1, 2024 at 14:0 Comment(11)
Do you have a full runnable minimal reproducible example? Can you post it somewhere like github? Does this actually result in NRE thrown when running the code?Modigliani
On the screenshots, one access "context" and the other "_context". But since I can't see the full code, I'm not able to say if this difference is important.Burnish
@Fildor: looks like just the Microsoft.AspNetCore.Mvc.ControllerBase to me. The usual class you inherit when implementing the webapi.Kuban
@WiktorZychla Jepp, may be. I think I got confused because I didn't see it in the second snippet, but it just may be outside of the shot.Digit
@Burnish yes, because in the first case the primary ctor parameter is used (C# 12 feature) and in the second "ordinary" field assigned from the ctor. Both should work in this case (basically the primary ctors feature was designed to reduce such boilerplate code).Modigliani
In general, this should work, in both cases. The request for a minimal reproducible example makes sense if we are supposed to help here.Kuban
Where does AddOutboxMessageEntity come from? MassTransit? Which version? How are the services and DataContext registered? The problem may be caused by MassTransit, or a missing registration.Elijah
@GuruStron here is a sample repo github.com/realShinchoku/DemoPrimaryCtorIsNullSopping
@WiktorZychla it actually works. It is just debug view issue when primary ctor parameter is used in async method.Modigliani
Hi @WiktorZychla,You can try to run this code in visual studio 17.9 preview 2.1 which has been solved such debugging view issue.Feminism
Thank to both of you, I know it works, I validated it locally after the question has been posted. That's why I joined the request for the reproducible example. Regards.Kuban
M
3

This is a bug in debug view both in Rider and Visual Studio:

Based on the provided repro (after some changes to setup) the primary constructor is working fine. Changed GetByRoom method to the following:

// var userId = Guid.Parse(User.GetObjectId());

var userId = Guid.NewGuid();
if (!await context.Students.AnyAsync(x => x.Id == userId))
{
    var isNull = context is null;
    Console.WriteLine(isNull);
    var localCopy = context; // for debug view
    return NotFound("Student not found");
}

As shown on the following screenshots the isNull is false and localCopy displays correct value:

enter image description here

enter image description here

Modigliani answered 11/1, 2024 at 9:29 Comment(5)
so it seems the service is not null, but the editor cannot display it, right? But why it displays the service if I remove the async/await?Feminism
@Feminism because it is Rider/Resharper bug involving Primary Ctors + async methods. It is better to be addressed there.Modigliani
Hi @Guru Stron, I test it in my visual studio without Resharper. Yes it actually have the value if I do like what you did var localCopy = context; , the context is null but the localCopy contains value.Feminism
@Feminism yes, found issue for VS too (added it to the answer). Again, it is only representational issue in the debug view and the code actually works.Modigliani
@jeb changed wording a bit.Modigliani

© 2022 - 2025 — McMap. All rights reserved.