Ali.Rashidi
Ali.Rashidi

Reputation: 1462

System.NotSupportedException: 'Collection is read-only.' When Reading Entity From DbContext

I have an aggregate root in my domain model named Employee. it has 3 list of different domain objects like below:

public class Employee: EntityBase, IAggregateRoot
{
    public long PersonnelCode { get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public string NationalCode { get; private set; }
    public DateTime Birthdate { get; private set; }
    public string Password { get; private set; }
    public string Address { get; private set; }

    public IReadOnlyList<Contract> Contracts => _Contracts.AsReadOnly();
    public IReadOnlyList<EmployeeShiftAssignment> EmployeeShiftAssignments => _ShiftAssignments.AsReadOnly();
    public IReadOnlyList<OverallWorkSummary> OverallWorkSummaries => _OverallWorkSummaries.AsReadOnly();

    private List<Contract> _Contracts = new List<Contract>();
    private List<EmployeeShiftAssignment> _ShiftAssignments = new List<EmployeeShiftAssignment>();
    private List<OverallWorkSummary> _OverallWorkSummaries=new List<OverallWorkSummary>();
}

this is the implementation of each object:

public class Contract:EntityBase
{
    public Contract(Guid employeeId,DateTime startDate,DateTime endDate)
    {

        this.EmployeeId = employeeId;
        
    }
    public Guid EmployeeId { get; private set; }
    public DateTime StartDate { get; private set; }
    public DateTime EndDate { get; private set; }
}


public class EmployeeShiftAssignment:ValueObject<EmployeeShiftAssignment>
{
    protected EmployeeShiftAssignment() { }
    public EmployeeShiftAssignment(Guid shiftId,DateTime assignmentDate,bool archived=false)
    {
        this.ShiftId = shiftId;
        this.AssignmentDate = assignmentDate;
    }

    public Guid ShiftId { get; private set; }
    public DateTime AssignmentDate { get; private set; }
    public bool Archived { get;private set; }
}


public class OverallWorkSummary : ValueObject<OverallWorkSummary>
{

    protected OverallWorkSummary() { }
    public OverallWorkSummary(
        DateTime startDate,
        DateTime endDate,
        double totalWorkInHours,
        double totalOvertimeInHours,
        double totalUndertimeInHours
        )
    {
        StartDate = startDate;
        EndDate = endDate;
        TotalWorkInHours = totalWorkInHours;
        TotalOvertimeInHours = totalOvertimeInHours;
        TotalUndertimeInHours = totalUndertimeInHours;
    }
    public DateTime StartDate { get; private set; }
    public DateTime EndDate { get; private set; }
    public double TotalWorkInHours { get; private set; }
    public double TotalOvertimeInHours { get; private set; }
    public double TotalUndertimeInHours { get; private set; }
}

and this is Employee Mapping configuration:

public class EmployeeMapping : EntityMappingBase<Employee>, IEntityMapping
{
    public override void Configure(EntityTypeBuilder<Employee> builder)
    {
        Initial(builder);
        builder.Property(i => i.FirstName)
            .IsRequired()
            .HasMaxLength(50);

        builder.Property(i => i.LastName)
            .IsRequired()
            .HasMaxLength(100);

        builder.Property(i => i.NationalCode)
            .IsRequired()
            .HasMaxLength(10);

        builder.Property(i => i.Address)
            .IsRequired();

        builder.Property(i => i.Birthdate)
            .IsRequired()
            .HasColumnType(nameof(SqlDbType.Date));

        builder.Property(i => i.Password)
            .IsRequired()
            .HasMaxLength(16);

        builder.Property(i => i.PersonnelCode)
            .IsRequired();

        builder.OwnsMany(i => i.EmployeeShiftAssignments, map =>
        {
            map.ToTable("EmployeeShiftAssignments", "EmployeeContext").HasKey("Id");
            map.Property<long>("Id").ValueGeneratedOnAdd();
            map.WithOwner().HasForeignKey("EmployeeId");

            map.Property(i=>i.AssignmentDate).IsRequired().HasColumnType(nameof(SqlDbType.Date));
            map.Property(i => i.Archived).IsRequired();
            map.UsePropertyAccessMode(PropertyAccessMode.Field);
        });

        builder.OwnsMany(i => i.OverallWorkSummaries, map =>
        {
            map.ToTable("OverallWorkSummaries", "EmployeeContext").HasKey("Id");
            map.Property<long?>("Id").ValueGeneratedOnAdd();
            map.WithOwner().HasForeignKey("EmployeeId");

            map.Property(i => i.EndDate).IsRequired();
            map.Property(i => i.StartDate).IsRequired();
            map.Property(i => i.TotalWorkInHours).IsRequired();
            map.Property(i => i.TotalUndertimeInHours).IsRequired();
            map.Property(i => i.TotalOvertimeInHours).IsRequired();
            map.UsePropertyAccessMode(PropertyAccessMode.Field);
        });
    }
}

public class ContractMapping : EntityMappingBase<Contract>,IEntityMapping
{
    public override void Configure(EntityTypeBuilder<Contract> builder)
    {
        Initial(builder);
        builder.Property(i => i.StartDate).IsRequired().HasColumnType(nameof(SqlDbType.Date));
        builder.Property(i => i.EndDate).IsRequired().HasColumnType(nameof(SqlDbType.Date));
        builder.HasOne<Employee>()
            .WithMany(i => i.Contracts)
            .HasForeignKey(i => i.EmployeeId)
            .HasConstraintName("FK_Employee_Contracts");
    }
}

and just in case you get curious:

public abstract class EntityMappingBase<TEntity> : IEntityTypeConfiguration<TEntity>,IEntityMapping
    where TEntity:EntityBase
{
    public abstract void Configure(EntityTypeBuilder<TEntity> builder);

    protected void Initial(EntityTypeBuilder<TEntity> builder)
    {
        builder.Property(i => i.Id)
            .HasColumnType(nameof(SqlDbType.UniqueIdentifier))
            .IsRequired()
            .ValueGeneratedNever();

        builder.HasKey(i => i.Id);

        builder.ToTable(typeof(TEntity).Name, typeof(TEntity).Namespace?.Split('.')[1]);

    }
}

enter image description here

with this configuration whenever I try to read employee it throws an exception saying that 'Collection is readonly' without specifying which Collection!.

when I change IReadOnlyList to List error is gone but I can't do it because due to conventions we have we can't expose aggregate's internal members to outside of it.

this is my repository where I get error

public class EmployeeRepository : RepositoryBase<Employee>, IEmployeeRepository
{
    public EmployeeRepository(IDbContext context) : base(context) { }

    public Employee FindById(Guid Id)
    {
        return dbContext.Set<Employee>()
            .Include(i => i.Contracts)
            .FirstOrDefault(i => i.Id == Id);//Throws Exception Here!
    }

    public List<Employee> GetAll()
    {
        var e = dbContext.Set<Employee>()
        .Include(i => i.Contracts)
        .ToList();//Throws Exception Here!
        return e;
    }
}

what is causing this exception?

Upvotes: 1

Views: 1501

Answers (1)

Ali.Rashidi
Ali.Rashidi

Reputation: 1462

Error was because of my invalid backing field naming.

By convention, the following fields will be discovered as backing fields for a given property (listed in precedence order).

_<camel-cased property name>
_<property name>
m_<camel-cased property name>
m_<property name>

In my domain object I have a field named which is not valid based on convention because it refers to <_ShiftAssignments> .

I renamed it to _EmployeeShiftAssignments and error was gone :)

Upvotes: 2

Related Questions