I am currently working on an ASP.NET Web API and I have encountered an issue related to the serialization of my entities. I have a Car class that references other classes, some of which contain their own Locales, such as Color and Transmission.
Here is the structure of my Car class:
public class Car : IEntity
{
public int Id { get; set; }
public int? OwnerId { get; set; }
public int BrandId { get; set; }
public int ModelId { get; set; }
public int ReleaseYearId { get; set; }
public int ColorId { get; set; }
public int MarketId { get; set; }
public int? RegionId { get; set; }
public int GearTypeId { get; set; }
public int CategoryId { get; set; }
public int FueltypeId { get; set; }
public int CurrencyId { get; set; }
public int? AutoSalonId { get; set; }
public int TransmissionId { get; set; }
public ushort EngineVolume { get; set; }
public ushort HorsePower { get; set; }
public DateTime LastUpdated { get; set; }
public string? Description { get; set; }
public int Mileage { get; set; }
public int MileageTypeId { get; set; }
public int Price { get; set; }
public ushort? SeatCount { get; set; }
public bool CreditAvailable { get; set; }
public bool BarterAvailable { get; set; }
public List<Image> Images { get; set; } = new();
public List<Feature> Features { get; set; } = new();
public User? Owner { get; set; }
public Brand? Brand { get; set; }
public Model? Model { get; set; }
public Year? Year { get; set; }
public Color? Color { get; set; }
public Region? Region { get; set; }
public Market? Market { get; set; }
public GearType? GearType { get; set; }
public Category? Category { get; set; }
public FuelType? Fueltype { get; set; }
public Currency? Currency { get; set; }
public MileageType? MileageType { get; set; }
public Transmission? Transmission { get; set; }
public AutoSalon? AutoSalon { get; set; }
public Car() { }
}
My Color and Transmission classes are structured like this:
public class Color : IEntity
{
public int Id { get; set; }
public List<ColorLocale>? ColorLocales { get; set; }
public Color() { }
}
public class ColorLocale : IEntity
{
public int Id { get; set; }
public int LanguageId { get; set; }
public string Name { get; set; } = string.Empty;
public Color? Color { get; set; }
public Language? Language { get; set; }
public ColorLocale() { }
}
public class Transmission : IEntity
{
public int Id { get; set; }
public List<TransmissionLocale>? TransmissionLocales { get; set; }
public Transmission() { }
}
public class TransmissionLocale : IEntity
{
public int Id { get; set; }
public int LanguageId { get; set; }
public string Name { get; set; } = string.Empty;
public Language? Language { get; set; }
public Transmission? Transmission { get; set; }
public TransmissionLocale() { }
}
When I try to get data from my database (which I have confirmed is there as I have debugged it), and return it from my controller, the process seems to hang due to the serialization not completing.
Here is the relevant part of my controller:
[HttpPost("getcars")]
public async Task<ActionResult<List<Car>>> GetCars(int pageNumber = 1, int pageSize = 20)
{
var carList = await _carService.GetCarsWithPagination("", pageNumber, pageSize);
return carList;
}
I have tried several methods to solve this, like setting Newtonsoft's ReferenceLoopHandling to Ignore or setting the JsonSerializerOptions.ReferenceHandler to Preserve, but neither has worked:
builder.Services.AddControllersWithViews()
.AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
builder.Services.AddControllersWithViews().AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
});
I am wondering if anyone has faced this issue before and could provide a solution. Are there any other settings I could tweak or alternate ways of structuring my classes to avoid this issue?
Any assistance is greatly appreciated.
Thank you!
The best approach is to not attempt serialize entities. Entities serve as a data domain representation and are not suited to data transfer. Keep these concerns separate and structure a DTO to represent the data structure you actually want to send, then use EF to project the entities to that DTO. Mappers like Automapper have hooks to work with EF's IQueryable
via ProjectTo<TDTO>()
to build efficient queries to populate these classes without the overhead of loading entire entity graphs into memory. If you take a DTO back to update an entity you can use the Map(src, dest)
method to copy allowed values back across to a freshly loaded entity.
When it comes to entities themselves I recommend using uni-directional references by default rather than bi-directional ones. One issue with trying to do something like serialize an entity is where a Transmission has a reference to one or more TransmissionLocale which have references back to a Transmission. Again I don't recommend serializing entities for data transmission, but there are often cases where I do want to serialize an entity, such as to record a snapshot of the entity in question such as when logging an exception, or an audit log snapshot. In most cases these references back are not necessary, and can reduce/remove situations where serialization gets caught up into loops. In most cases you can resolve queries using uni-directional references just fine. For instance if you want all transmissions for a particular locale:
Bi-directional:
var transmissions = _context.TransmissionLocales
.Where(x => x.LocaleId == localeId)
.Select(x => x.Transmission)
.ToList();
Uni-directional:
var transmissions = _context.Transmissions
.Where(x => x.Locales.Any(l => l.LocaleId == localeId)
.ToList();
Personally I find using uni-directional references more direct as when selecting entities of a particular type I am going to their DbSet, not a linking table, and if I am getting items that are related to a particular top-level entity I go through that top-level entity. So for instance if I want all cars with a transmission of a specific locale:
var cars = _context.Cars
.Where(x => x.Transmissions.Any(t => t.Locales.Any(l => l.LocaleId == localeId)
.ToList();
Often conditions can be combined across concerns with AND or OR so if I am looking for Cars, I want to do querying from Car down to what makes up a Car so I can combine conditions, rather than trying to select Cars from querying their components. For instance if I want cars with a transmission or gear type of a particular locale.