ASP.NET Web API - AutoMapper is not ignoring nullable booleans - c#

I'm writting web application in ASP.NET Web API and working on method to modify restaurant informations. The core entity is Restaurant that looks like this:
public class Restaurant
{
public int Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public string Category { get; set; }
public bool HasDelivery { get; set; }
public string ContactEmail { get; set; }
public string ContactNumber { get; set; }
public int? CreatedById { get; set; }
public virtual User CreatedBy { get; set; }
public int AddressId { get; set; }
public virtual Address Address { get; set; }
public virtual List<Dish> Dishes { get; set; }
}
What is the most important in here - the HasDelivery property must be not-nullable. It has to take one of true or false value.
Next, I have ModifyRestaurantDto class which is given as a request from body while the app working. Just like the following:
public class ModifyRestaurantDto
{
[MaxLength(25)]
public string? Name { get; set; }
[MaxLength(100)]
public string? Description { get; set; }
public bool? HasDelivery { get; set; }
}
To simplify I've just given a few of properties that are allowed to be changed. Note that all of them are nullable types.
I also have a service method called UpdateAsync, as follows:
public async Task UpdateAsync(int id, ModifyRestaurantDto modifyRestaurantDto)
{
var restaurant = await _dbContext
.Restaurants
.FirstOrDefaultAsync(r => r.Id == id)
?? throw new NotFoundException("Restaurant not found...");
var authorizationResult = _authorizationService
.AuthorizeAsync(
_userContextService.User,
restaurant,
new ResourceOperationRequirement(ResourceOperation.Update))
.Result;
if (!authorizationResult.Succeeded)
throw new ForbidException();
_mapper.Map(modifyRestaurantDto, restaurant);
await _dbContext.SaveChangesAsync();
}
What I want to achieve is to change only values which has been given in request body (in ModifyRestaurantDto). E.g. if my json body was looking like this
{
"name": "Foo"
}
I wouldn't want Description and HasDelivery props to change.
Now, I've created AutoMapper profile and configured it inside Program.cs of course.
public class RestaurantMappingProfile : Profile
{
public RestaurantMappingProfile()
{
CreateMap<ModifyRestaurantDto, Restaurant>()
.ForAllMembers(opts => opts.Condition((src, dest, value) => value is not null));
}
}
And while given types are string everything works correctly. The problem is that nullable bool is always converted to false anyways. I'm using .NET 6.0 and have enabled Nullable and ImplicitUsings in .csproj.
Do you have some idea why only nullable booleans are not overlooked by AutoMapper?

It seems like a bug to me. A quick fix would be adding mapping from bool? to bool:
CreateMap<bool?, bool>().ConvertUsing((src, dest) => src ?? dest);
CreateMap<ModifyRestaurantDto, Restaurant>()
.ForAllMembers(opts => opts.Condition((_, _, v) => v is not null));

Related

C# include relationship object from many to many with automapper

I'm trying to map from Role to RoleModel including the Feature/FeatureModel, but I haven't been able to make it work properly.
It returns the role but does not include the features:
{
"name": "Backend",
"description": "Backend role",
"roleFeatures": [],
"roleUsers": [],
"updatedDate": "2023-02-03T16:14:54.036441",
"deletedDate": null,
"id": "82a443bd-81d3-4460-8a67-08db0601365e",
"createdDate": "2023-02-03T16:14:54.036441",
"deleted": false
}
This is the map I created:
CreateMap<Role, RoleModel>()
.ForMember(rm => rm.RoleFeatures, opt => opt
.MapFrom(r => r.RoleFeatures.Select(y => y.Feature).ToList()))
.MaxDepth(1);
These are the classes:
Role
public class Role : BaseRecord
{
public string Name { get; set; }
public string Description { get; set; }
public ICollection<RoleFeature> RoleFeatures { get; set; }
public ICollection<RoleUser> RoleUsers { get; set; }
}
RoleModel
public class RoleModel : BaseRecordModel
{
public string Name { get; set; }
public string Description { get; set; }
public ICollection<RoleFeatureModel> RoleFeatures { get; set; }
public ICollection<RoleUserModel> RoleUsers { get; set; }
}
RoleFeature
public class RoleFeature
{
public bool Read { get; set; }
public bool Write { get; set; }
public bool Edit { get; set; }
public bool Delete { get; set; }
public Guid RoleId { get; set; }
public Role Role { get; set; }
public Guid FeatureId { get; set; }
public Feature Feature { get; set; }
}
RoleFeatureModel
public class RoleFeatureModel
{
public Guid RoleId { get; set; }
public Guid FeatureId { get; set; }
public bool Read { get; set; }
public bool Write { get; set; }
public bool Edit { get; set; }
public bool Delete { get; set; }
public RoleModel Role { get; set; }
public FeatureModel Feature { get; set; }
}
Previously I was creating a simple map:
CreateMap<Role, RoleModel>();
But I was getting an object cycle error, now I get the roleModel but the roleFeature list is empty.
I'm gonna listen to what my guts tell me - you are querying for the roles in lazy loading manner (default), aren't you?
You have to include related members explicitly, for example:
var resultSet = await _context.Roles.Include(r => r.RoleFeatures).ToListAsync();
Include() method enforces so called eager loading, providing you not only with requested data, but also its related members.
It also can be chained e.g.:
var resultSet = _context.Roles.Include(r => r.RoleFeatures)
.Include(r => r.RoleUsers)
.ToListAsync();
OR
If you are already using eager loading.
Instead of constraining your mapper with MaxDepth(1), go to your ConfigureServices() (in your Startup.cs or Program.cs) method, and configure JsonSerializer in a way that suits you the best, for example:
services.AddNewtonsoftJson(options =>
{
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
options.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize;
options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All;
});
Above example uses Newtonsoft.Json, but you can achieve the same effect reffering ReferenceLoopHandling with System.Text.Json serializer.

How to make out of three property only two can be null in C#

I am new to asp.net core web api. I want to make announcement module for my school project. What i want to make is that admins can send announcement based on role or target user or target section. when the request come it must contain at least one not null, How can I achieve this in the class property.
public class Announcement
{
public int Id { get; set; }
public string Title { get; set; }
public string Detail { get; set; }
public DateTime Date { get; set; }
public string? Attachment { get; set; }
public AppUser Poster { get; set; }
public string PosterUserId { get; set; }
public AppUser? Receiver { get; set; }
public string? ReceiverUserId { get; set; }
public Section? Section { get; set; }
public int? SectionId { get; set; }
public IdentityRole? Role { get; set; }
public string? RoleId { get; set; }
}
I can achieve this in the controller but, Is there any method I can use so that the model binder will reject it if three of them are null.
Going by the documentation #Rand Random linked. You want to make your class an IValidatableObject and then implement the Validate method. Something like this:
public class Announcement : IValidatableObject
{
...
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (IdentityRole == null && Section == null && Receiver == null)
{
yield return new ValidationResult(
$"You must have at least one field of Role, Target User, or Target Section selected.",
new[] { nameof(IdentityRole), nameof(Section), nameof(Receiver) });
}
}
}

Generic function given showing exception

I wrote following repository function in the repository and it is not showing the data.
public async Task<IEnumerable<User>> GetAllUser()
{
return await FindByCondition(ur => ur.IsDeleted != true).Include(x => x.UserRole.RoleType).ToListAsync();
}
Generic function for this is:
public IQueryable<TEntity> FindByCondition(Expression<Func<TEntity, bool>> expression)
{
return _mBHDbContext.Set<TEntity>().Where(expression).AsNoTracking();
}
It shows exception when writing the above code:
This error comes when "include" is using with the query. Means when we need data from two tables the problem showing.
And my entity model structure is look like:
public partial class User
{
public User()
{
PatientAnswers = new HashSet<PatientAnswer>();
}
public long UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool? IsDeleted { get; set; }
public int? UserRoleId { get; set; }
public DateTime? DataOfBirth { get; set; }
public virtual RoleMaster UserRole { get; set; }
public virtual ICollection<PatientAnswer> PatientAnswers { get; set; }
}
and other table structure is look like:
public partial class RoleMaster
{
public RoleMaster()
{
Users = new HashSet<User>();
}
public int Id { get; set; }
public string RoleType { get; set; }
public virtual ICollection<User> Users { get; set; }
}
Try using the below. This brings the information inside linked UserRole entity and avoids a round trip when accessing RoleType
public async Task<IEnumerable<User>> GetAllUser()
{
return await FindByCondition(ur => ur.IsDeleted != true).Include(x => x.UserRole).ToListAsync();
}
You can use ThenInclude like this:
public async Task<IEnumerable<User>> GetAllUser()
{
return await FindByCondition(ur => ur.IsDeleted != true)
.OrderBy(ro => ro.UserId)
.Include(x => x.UserRoleNavigation)
.ThenInclude(ur => ur.RoleType)
.ToListAsync();
}
It complains about Include(x => x.UserRole.RoleType) expression.
Include expect lambda poiting navigation property to include, so as I see it it's x.UserRole.
Moreover it does not make sense to Inlucde value property, such as string. If you want to access it (fetch it from db), it's enought to include navigation property that contains it, so after all, you need only Include(x => x.UserRole)

JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than

In my web API when I run project to get data from the database got this error
.net core 3.1
JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32.
These are my codes:
my Model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string ProductText { get; set; }
public int ProductCategoryId { get; set; }
[JsonIgnore]
public virtual ProductCategory ProductCategory { get; set; }
}
my productCategory class is:
public class ProductCategory
{
public int Id { get; set; }
public string Name { get; set; }
public string CatText { get; set; }
public string ImagePath { get; set; }
public int Priority { get; set; }
public int Viewd { get; set; }
public string Description { get; set; }
public bool Active { get; set; }
public DateTime CreateDate { get; set; }
public DateTime ModifyDate { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
my repo is
public async Task<IList<Product>> GetAllProductAsync()
{
return await _context.Products.Include(p => p.ProductCategory).ToListAsync();
}
my interface
public interface IProductRepository
{
...
Task<IList<Product>> GetAllProductAsync();
...
}
and this is my controller in api project
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _productRepository;
public ProductsController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
[HttpGet]
public ActionResult Get()
{
return Ok(_productRepository.GetAllProduct());
}
}
When I run API project and put this URL: https://localhost:44397/api/products
I got that error,
I can't resolve it
this is happening because your data have a reference loop.
e.g
// this example creates a reference loop
var p = new Product()
{
ProductCategory = new ProductCategory()
{ products = new List<Product>() }
};
p.ProductCategory.products.Add(p); // <- this create the loop
var x = JsonSerializer.Serialize(p); // A possible object cycle was detected ...
You can not handle the reference loop situation in the new System.Text.Json yet (netcore 3.1.1) unless you completely ignore a reference and its not a good idea always. (using [JsonIgnore] attribute)
but you have two options to fix this.
you can use Newtonsoft.Json in your project instead of System.Text.Json (i linked an article for you)
Download the System.Text.Json preview package version 5.0.0-alpha.1.20071.1 from dotnet5 gallery (through Visual Studio's NuGet client):
option 1 usage:
services.AddMvc()
.AddNewtonsoftJson(
options => {
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
// if you not using .AddMvc use these methods instead
//services.AddControllers().AddNewtonsoftJson(...);
//services.AddControllersWithViews().AddNewtonsoftJson(...);
//services.AddRazorPages().AddNewtonsoftJson(...);
option 2 usage:
// for manual serializer
var options = new JsonSerializerOptions
{
ReferenceHandling = ReferenceHandling.Preserve
};
string json = JsonSerializer.Serialize(objectWithLoops, options);
// -----------------------------------------
// for asp.net core 3.1 (globaly)
services.AddMvc()
.AddJsonOptions(o => {
o.JsonSerializerOptions
.ReferenceHandling = ReferenceHandling.Preserve
});
these serializers have ReferenceLoopHandling feature.
Edit : ReferenceHandling changed to ReferenceHandler in DotNet 5
but if you decide to just ignore one reference use [JsonIgnore] on one of these properties. but it causes null result on your API response for that field even when you don't have a reference loop.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string ProductText { get; set; }
public int ProductCategoryId { get; set; }
// [JsonIgnore] HERE or
public virtual ProductCategory ProductCategory { get; set; }
}
public class ProductCategory
{
public int Id { get; set; }
// [JsonIgnore] or HERE
public ICollection<Product> products {get;set;}
}
.NET 5 Web API
public static void ConfigureServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddControllers()
.AddJsonOptions(o => o.JsonSerializerOptions
.ReferenceHandler = ReferenceHandler.Preserve);
}
I have the same issue, my fix was to add async and await keyword since I am calling an async method on my business logic.
Here is my original code:
[HttpGet]
public IActionResult Get()
{
//This is async method and I am not using await and async feature .NET which triggers the error
var results = _repository.GetAllDataAsync();
return Ok(results);
}
To this one:
HttpGet]
public async Task<IActionResult> Get()
{
var results = await _repository.GetAllDataAsync();
return Ok(results);
}
In .Net 6, you can use System.Text.Json to initialize a startup action with AddControllersWithViews like this in Program.cs,
using System.Text.Json.Serialization;
builder.Services.AddControllersWithViews()
.AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
also you can use AddMvc like this,
builder.Services.AddMvc()
.AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
but quote from Ryan
asp.net core 3.0+ template use these new
methodsAddControllersWithViews,AddRazorPages,AddControllers instead of
AddMvc.
I will recommend to use the first solution.
Ensure you have [JsonIgnore] on the correct fields to avoid a circular reference.
In this case you will need
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string ProductText { get; set; }
[JsonIgnore]
public virtual ProductCategory ProductCategory { get; set; }
}
You probably don't need the ProductCategoryId field (depends if you are using EF and code first to define your DB)
Edit - In answer to noruk
There is often confusion in connected objects and navigation properties. You can get the data you want in JSON but also define the EF structures to get the correct DB structure (foreign keys, indexes, etc).
Take this simple example. A Product (for example a T-Shirt) has many sizes or SKUs (e.g. Small, Large, etc)
public class Product
{
[Key]
[MaxLength(50)]
public string Style { get; set; }
[MaxLength(255)]
public string Description { get; set; }
public List<Sku> Skus { get; set; }
}
public class Sku
{
[Key]
[MaxLength(50)]
public string Sku { get; set; }
[MaxLength(50)]
public string Barcode { get; set; }
public string Size { get; set; }
public decimal Price { get; set; }
// One to Many for Product
[JsonIgnore]
public Product Product { get; set; }
}
Here you can serialise a Product and the JSON data will include the SKUs. This is the normal way of doing things.
However if you serialise a SKU you will NOT get it's parent product. Including the navigation property will send you into the dreaded loop and throw the "object cycle was detected" error.
I know this is limiting in some use cases but I would suggest you follow this pattern and if you want the parent object available you fetch it separately based on the child.
var parent = dbContext.SKUs.Include(p => p.Product).First(s => s.Sku == "MY SKU").Product
I fixed my API Core Net6.0 adding [JsonIgnore]:
public class SubCategoryDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Image { get; set; }
public int CategoryId { get; set; }
[JsonIgnore]
public Category Category { get; set; }
}
For net core 3.1 you have to add in Startup.cs:
services.AddMvc.AddJsonOptions(o => {
o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
o.JsonSerializerOptions.MaxDepth = 0;
})
and import at least this package using nuget.org include prerelease:
<PackageReference Include="System.Text.Json" Version="5.0.0-rc.1.20451.14" />
following code is working for me in dotnet 5.0 :
services.AddControllersWithViews()
.AddJsonOptions(o => o.JsonSerializerOptions
.ReferenceHandler = ReferenceHandler.Preserve);
Finally fixed mine with System.Text.Json not NewtonSoft.Json using
var options = new JsonSerializerOptions()
{
MaxDepth = 0,
IgnoreNullValues = true,
IgnoreReadOnlyProperties = true
};
Using options to serialize
objstr = JsonSerializer.Serialize(obj,options);
My project built with a similar error.
Here's the code before
public class PrimaryClass {
public int PrimaryClassId
public ICollection<DependentClass> DependentClasses { get; set; }
}
public class DependentClass {
public int DependentClassId { get; set; }
public int PrimaryClassId { get; set; }
public PrimaryClass primaryClass { get; set; }
}
I took away the PrimaryClass object from the DependentClass model.
Code after
public class PrimaryClass {
public int PrimaryClassId
public ICollection<DependentClass> DependentClasses { get; set; }
}
public class DependentClass {
public int DependentClassId { get; set; }
public int PrimaryClassId { get; set; }
}
I also had to adjust the OnModelCreating method from
modelBuilder.Entity<PrimaryClass>().HasMany(p => p.DependentClasses).WithOne(d => d.primaryClass).HasForeignKey(d => d.PrimaryClassId);
to
modelBuilder.Entity<PrimaryClass>().HasMany(p => p.DependentClasses);
The DbSet query that's running is
public async Task<List<DependentClass>> GetPrimaryClassDependentClasses(PrimaryClass p)
{
return await _dbContext.DependentClass.Where(dep => dep.PrimaryClassId == p.PrimaryClassId).ToListAsync();
}
The error could have been with any of these 3 sections of code, but removing the primary object reference from the dependent class and adjusting the OnModelCreating resolved the error, I'm just not sure why that would cause a cycle.
In my case the problem was when creating the entity relationships. I linked the main entity using a foreign key inside the dependent entity like this
[ForeignKey("category_id")]
public Device_Category Device_Category { get; set; }
also I referred the dipendend entity inside the main entity as well.
public List<Device> devices { get; set; }
which created a cycle.
Dependent Entity
public class Device
{
[Key]
public int id { get; set; }
public int asset_number { get; set; }
public string brand { get; set; }
public string model_name { get; set; }
public string model_no { get; set; }
public string serial_no { get; set; }
public string os { get; set; }
public string os_version { get; set; }
public string note { get; set; }
public bool shared { get; set; }
public int week_limit { get; set; }
public bool auto_acceptance { get; set; }
public bool booking_availability { get; set; }
public bool hide_device { get; set; }
public bool last_booked_id { get; set; }
//getting the relationships category 1 to many
public int category_id { get; set; }
[ForeignKey("category_id")]
public Device_Category Device_Category { get; set; }
public List<Booking> Bookings { get; set; }
}
Main Entity
public class Device_Category
{
public int id { get; set; }
public string name { get; set; }
public List<Device> devices { get; set; }
}
}
So I commented the
public List<Device> devices { get; set; }
inside main entity (Device_Category) and problem solved

Invoking child nodes Entity Framework Core

I'm new into this sort of object programming, do you mind helping me?
I have to invoke the child nodes, but specific fields, but I can't figure out how to do that. I have already looked into a huge number of places, but it doesn't seem to work.
For example, I want just the FirstName in the RepunicAccount, and all the other info from RepunicAccountType, or anything like it.
My DbContext:
public class RepunicContext : DbContext
{
public virtual DbSet<RepunicAccount> RepunicAccount { get; set; }
public virtual DbSet<RepunicAccountType> RepunicAccountType { get; set; }
protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer ("Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=Repunic;Data Source=DESKTOP- 6I8LD45\\SQLEXPRESS_ERICH");
}
public RepunicContext (DbContextOptions<RepunicContext> options) :base (options)
{ }
public RepunicContext () { }
}
My model class:
public class RepunicAccount
{
public int ID { get; set; }
public string Email { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[ForeignKey ("ID_Type")]
public int? ID_Type { get; set; }
public ICollection<RepunicAccountType> TipoConta { get; set; }
public DateTime? DataCadastro { get; set; } = DateTime.Now;
public DateTime? DataAlteracao { get; set; } = DateTime.Now;
}
My child node class:
public class RepunicAccountType
{
[Key]
public int ID_Type { get; set; }
public string Descricao { get; set; }
public DateTime DataCadastro { get; set; } = DateTime.Now;
public DateTime DataAlteracao { get; set; } = DateTime.Now;
}
I have a repository, that is where I do my coding, before putting into the controller, it's pretty much the same in every place, but I'm going to show the example that I'm trying to use in both places.
public IEnumerable<RepunicAccount> GetAllByIDType ()
{
var data = db.RepunicAccount.Where (a => a.ID_Type != null)
.Include (p => p.types);
var type = db.RepunicAccountType.OrderBy (b => b.Descricao);
return data.ToList();
}
The problem is: I dont know how to invoke specific items nor make anything else other thanToList();`
So, what should I do? If there is any more info that I can send, just ask me.
I would love to comment however, I do not have the required reputation to do so.
Since you are new to the subject please be sure to follow the documentation.
EF Core Documentation
I also have created a youtube video for getting started that I would suggest you take a few minutes to view.
Video on EF Core
in short, you need a DBContext and your Models. You then can start a query for data and returning in the format you wish. Like so:
using (var context = new RepunicContext())
{
var accounts = context.RepunicAccounts.ToList();
}
accounts will then have a list of every account that you can iterate through. Hope this helps.

Categories

Resources