Search code examples
c#asp.net-coreforeign-keysxmlserializer

.NET Core, circular references on particular entity using XMLParser


I am using C# .Net Core along with the XMLSerializer, I've checked various other questions but none of the provided answers seem to work for me. I Have a MaintenanceMoment that has multiple time blocks, so a one-to-many relation, I've created it like the following:

My Model code:

PRMaintenanceMoment.cs

    public class PRMaintenanceMoment
    {
      [XmlIgnore]
      public int ID { get; set; }

      [Required]
      [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
      public string EarliestExecutionDate { get; set; }

      [Required]
      [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
      public string LatestExecutionDate { get; set; }

      public List<Timeblock> Timeblock { get; set; } = new List<Timeblock>();
    }

Timeblock.cs

public class Timeblock
{
    [XmlIgnore]
    public int ID { get; set; }

    [ForeignKey("MaintenanceMomentID")]
    public PRMaintenanceMoment MaintenanceMoment { get; set; }

    [Required]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:hh:mm:ss}")]
    public DateTime EarliestExecutionTime { get; set; }

    [Required]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:hh:mm:ss}")]
    public DateTime LatestExecutionTime { get; set; }
}

The reference in my Database Context has been made like this: ApplicationDbContext.cs

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }

    public DbSet<Timeblock> Timeblocks { get; set; }

 protected override void OnModelCreating(ModelBuilder modelBuilder)
 {
        modelBuilder.Entity<PRMaintenanceMoment>()
            .HasMany(tb => tb.Timeblock)
            .WithOne(pr => pr.MaintenanceMoment);
 }
}

My Controller that servers the MaintenanceMoment is created like so (I've excluded the generic repo & interface as they don't matter for this example):

PRMaintenanceMoment.cs

    public class PRMaintenanceMomentRepository : GenericRepository<PRMaintenanceMoment>, IPRMaintenanceMomentRepository
{
    public PRMaintenanceMomentRepository(ApplicationDbContext context)
        : base(context) { }

    public override List<PRMaintenanceMoment> Index()
    {
        var context = _context.PRMaintenanceMoments
               .Include(PRMaintenanceMoment => PRMaintenanceMoment.Timeblock)
            .ToList();

        return context;
    }

    public void Update(int id, PRMaintenanceMoment maintenanceMoment)
    {
        PRMaintenanceMoment DBPRMaintenanceMoment = _context.PRMaintenanceMoments.FirstOrDefault(ms => ms.ID.Equals(id));

        _context.Entry<PRMaintenanceMoment>(DBPRMaintenanceMoment).CurrentValues.SetValues(maintenanceMoment);

        _context.SaveChanges();
    }
}

The eventual Migration that is created shows the following:

migrationBuilder.CreateTable(
            name: "PRMaintenanceMoments",
            columns: table => new
            {
                ID = table.Column<int>(nullable: false)
                    .Annotation("MySql:ValueGenerationStrategy", 
                     MySqlValueGenerationStrategy.IdentityColumn),
                EarliestExecutionDate = table.Column<string>(nullable: false),
                LatestExecutionDate = table.Column<string>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_PRMaintenanceMoments", x => x.ID);
            });

        migrationBuilder.CreateTable(
            name: "Timeblocks",
            columns: table => new
            {
                ID = table.Column<int>(nullable: false)
                    .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
                MaintenanceMomentID = table.Column<int>(nullable: true),
                EarliestExecutionTime = table.Column<DateTime>(nullable: false),
                LatestExecutionTime = table.Column<DateTime>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Timeblocks", x => x.ID);
                table.ForeignKey(
                    name: "FK_Timeblocks_PRMaintenanceMoments_MaintenanceMomentID",
                    column: x => x.MaintenanceMomentID,
                    principalTable: "PRMaintenanceMoments",
                    principalColumn: "ID",
                    onDelete: ReferentialAction.Restrict);
            });

        migrationBuilder.CreateIndex(
            name: "IX_PlanningRequests_PRMaintenanceMomentID",
            table: "PlanningRequests",
            column: "PRMaintenanceMomentID");

        migrationBuilder.CreateIndex(
            name: "IX_Timeblocks_MaintenanceMomentID",
            table: "Timeblocks",
            column: "MaintenanceMomentID");

Which seems quite alright to me, then, as many questions suggest is adding the ReferenceLoopHandling from the JSON serializer. I've tried this but this didn't seem to help. For completion's sake, I've added the most important bits of my startup.cs file; this is of course not the full file, but the most important settings.

startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers()
             .AddXmlSerializerFormatters(); // Adding the XML serializer here

        services.AddMvc(options =>
        {
            options.MaxValidationDepth = 64;
            options.InputFormatters.Add(new XmlSerializerInputFormatter(options));
            options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
        });

services.AddMvc().AddNewtonsoftJson(jsonOptions => { jsonOptions.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;  });

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

Finally the exact error thrown is:

System.InvalidOperationException: There was an error generating the XML document.
---> System.InvalidOperationException: A circular reference was detected while serializing an object of type SALES005.Models.PRMaintenanceMoment.
at System.Xml.Serialization.XmlSerializationWriter.WriteStartElement(String name, String ns, Object o, Boolean writePrefixed, XmlSerializerNamespaces xmlns)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterList1.Write3_PRMaintenanceMoment(String n, String ns, PRMaintenanceMoment o, Boolean isNullable, Boolean needType)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterList1.Write2_Timeblock(String n, String ns, Timeblock o, Boolean isNullable, Boolean needType)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterList1.Write3_PRMaintenanceMoment(String n, String ns, PRMaintenanceMoment o, Boolean isNullable, Boolean needType)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterList1.Write4_ArrayOfPRMaintenanceMoment(Object o)

Solution

  • Newtonsoft can solve the json circular reference, but it can not solve the xml. So you can change the query method.

    public List<PRMaintenanceMoment> get6()
        {
            var pRMaintenanceMoments = (from prm in _db.PRMaintenanceMoment
                         
                       select new PRMaintenanceMoment
                       {
                           ID=prm.ID,
                            EarliestExecutionDate=prm.EarliestExecutionDate,
                             LatestExecutionDate=prm.LatestExecutionDate,
                             Timeblock=prm.Timeblock.Select(c=>new Timeblock
                             {
                                  ID=c.ID,
                             }).ToList()
                              
                       }).ToList();
            return pRMaintenanceMoments;
        }
    

    enter image description here