I am having an issue using an AutoMapper (version 5.1.1) projection combined with a Linq OrderBy Child property expression. I am using Entity Framework Core (version 1.0.0). I am getting the following error:
"must be reducible node"
My DTO objects are as follows
public class OrganizationViewModel
{
public virtual int Id { get; set; }
[Display(Name = "Organization Name")]
public virtual string Name { get; set; }
public virtual bool Active { get; set; }
public virtual int OrganizationGroupId { get; set; }
public virtual string OrganizationGroupName { get; set; }
public virtual int StrategyId { get; set; }
public virtual string StrategyName { get; set; }
public virtual OrganizationGroupViewModel OrganizationGroup { get; set; }
}
public class OrganizationGroupViewModel
{
public virtual int Id { get; set; }
[Display(Name = "Organization Group Name")]
public virtual string Name { get; set; }
public virtual bool Active { get; set; }
}
My corresponding entity models are as follows:
public class Organization
{
public int Id { get; set; }
public string Name { get; set; }
public string TimeZone { get; set; }
public bool Active { get; set; }
//FKs
public int OrganizationGroupId { get; set; }
public int StrategyId { get; set; }
//Navigation
public virtual OrganizationGroup OrganizationGroup { get; set; }
public virtual Strategy Strategy { get; set; }
[JsonIgnore]
public virtual List<AppointmentReminder> AppointmentReminders { get; set; }
}
public class OrganizationGroup
{
public int Id { get; set; }
public string Name { get; set; }
public bool Active { get; set; }
public virtual List<Organization> Organizations { get; set; }
}
My AutoMapper profiles are as follows:
public class OrganizationMapperProfile : Profile
{
public OrganizationMapperProfile()
{
CreateMap<Task<Organization>, Task<OrganizationViewModel>>();
CreateMap<Organization, OrganizationViewModel>()
.ForMember(dest => dest.OrganizationGroupName, opt => opt.MapFrom(src => src.OrganizationGroup.Name));
CreateMap<OrganizationInput, Organization>()
.ForMember(x => x.Id, opt => opt.Ignore());
}
}
public class OrganizationGroupMapperProfile : Profile
{
public OrganizationGroupMapperProfile()
{
CreateMap<Task<OrganizationGroup>, Task<OrganizationGroupViewModel>>();
CreateMap<OrganizationGroup, OrganizationGroupViewModel>();
CreateMap<OrganizationGroupInput, OrganizationGroup>()
.ForMember(x => x.Id, opt => opt.Ignore());
}
}
When I run the following statements I am able to run and get results from the first 2 statements:
var tmp = await _context.Organizations.Include(x => x.OrganizationGroup).OrderBy(x => x.OrganizationGroup.Name).ToListAsync();
var tmp4 = await _context.Organizations.Include(x => x.OrganizationGroup).OrderBy("OrganizationGroup.Name").ToListAsync();
But when I add the ProjectTo I get the error listed above:
var tmp5 = await _context.Organizations.Include(x => x.OrganizationGroup).OrderBy(x => x.OrganizationGroup.Name).ProjectTo<OrganizationViewModel>().ToListAsync();
var tmp6 = await _context.Organizations.Include(x => x.OrganizationGroup).OrderBy("OrganizationGroup.Name").ProjectTo<OrganizationViewModel>().ToListAsync();
As some additional information, I am able to OrderBy with Projections working on properties of the parent class, such as:
var tmp7 = await _context.Organizations.Include(x => x.OrganizationGroup).OrderBy(x => x.Name).ProjectTo<OrganizationViewModel>().ToListAsync();
var tmp8 = await _context.Organizations.Include(x => x.OrganizationGroup).OrderBy("Name").ProjectTo<OrganizationViewModel>().ToListAsync();
Anyone run into this issue before? Looks like I'm trying to do something that is otherwise not supported, is that by design? Thanks for any help/insight.
Looks like the problem is caused by the OrganizationGroup property of the OrganizationViewModel class - AutoMapper generates a null check which EF Core doesn't like in the combination with your OrderBy (I guess just one of the many bugs currently in EF Core). It can easily be reproduced by the following simple manual projection query:
var tmp5a = _context.Organizations
.OrderBy(x => x.OrganizationGroup.Name)
.Select(e => new OrganizationViewModel
{
Id = e.Id,
OrganizationGroup = e.OrganizationGroup != null ? new OrganizationGroupViewModel
{
Id = e.OrganizationGroup.Id,
Name = e.OrganizationGroup.Name,
Active = e.OrganizationGroup.Active,
} : null,
})
.ToList();
To fix the issue, configure AutoMapper to not generate null check for that property as follows:
CreateMap<Organization, OrganizationViewModel>()
.ForMember(dest => dest.OrganizationGroup, opt => opt.AllowNull())
.ForMember(dest => dest.OrganizationGroupName, opt => opt.MapFrom(src => src.OrganizationGroup.Name));
Related
I'm using last version of json implementation in Pomelo 5.0 and configure my maria server to use microsoft json serialisation.
var serverVersion = new MariaDbServerVersion(new Version(10, 3, 0));
services.AddDbContext<BusinessManagementDbContext>(options =>
{
options.UseMySql(databaseConfiguration.ConnectionString, serverVersion, m =>
{
m.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically);
m.EnableRetryOnFailure();
});
options.EnableSensitiveDataLogging(true);
});
I can save my POCO in my db but when I try to query my data, I get a null object.
Here's my data :
HeidySQL data
My query is pretty simple but I think I'm not using the right way for json query.
await Context.ValidatedSaleInvoices.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
It seems like there is no deserialization between my data and my property "Content".
How can I do this ?
Thank you,
Edit
My model is :
public class ValidateSaleInvoiceEntity
{
public int Id { get; set; }
public ValidateSaleInvoiceContent Content { get; set; }
}
public class ValidateSaleInvoiceContent
{
public string BusinessName { get; set; }
public DateTime Date { get; internal set; }
public string Number { get; internal set; }
public Address Address { get; internal set; }
public List<ValidateSaleInvoiceLineEntity> Lines { get; internal set; } = new List<ValidateSaleInvoiceLineEntity>();
}
public class ValidateSaleInvoiceLineEntity
{
public string Description { get; internal set; }
public decimal Quantity { get; internal set; }
public decimal UnitPriceVatExcluded { get; internal set; }
public decimal VatRate { get; internal set; }
}
And my json Result was like this (empty, like there waere no deserialisation: { "BusinessName":"", "Date":"", "Number":"" etc.}
My boss stop my poc about MariaDB Json implementation so I had to go back to this good old friend pure sql column :/ . That's why I haven"t a full json result. Sorry
For a property to serialize/deserialize JSON automatically to a POCO, you need to tell Pomelo, that the table column of the property is of the MySQL/MariaDB type json:
public class ValidateSaleInvoiceEntity
{
public int Id { get; set; }
[Column(TypeName = "json")] // <-- this is one way to do it
public ValidateSaleInvoiceContent Content { get; set; }
}
public class MyContext : DbContext
{
// ...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ValidateSaleInvoiceEntity>()
.Property(e => e.Content)
.HasColumnType("json"); // <-- this is another way to do it
}
}
Here is a fully working console project:
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
// EF Core entities:
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
// Either use this data annotation, or the corresponding Fluent API call (see
// OnModelCreating), to explicitly mark the column type as JSON.
[Column(TypeName = "json")]
public IceCreamDetails Details { get; set; }
}
// JSON class:
public class IceCreamDetails
{
public int Kilojoule { get; set; }
public int Rating { get; set; }
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=So68020732";
var serverVersion = ServerVersion.AutoDetect(connectionString);
optionsBuilder.UseMySql(connectionString, serverVersion, options => options
.UseMicrosoftJson(MySqlCommonJsonChangeTrackingOptions.FullHierarchyOptimizedSemantically))
.UseLoggerFactory(
LoggerFactory.Create(
configure => configure
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>(
entity =>
{
// Either use this Fluent API call, or the corresponding data annotation
// (see the IceCreamDetails class), to explicitly mark the column type as JSON.
entity.Property(e => e.Details)
.HasColumnType("json");
entity.HasData(
new IceCream {IceCreamId = 1, Name = "Vanilla", Details = new IceCreamDetails { Kilojoule = 865, Rating = 9 }},
new IceCream {IceCreamId = 2, Name = "Chocolate", Details = new IceCreamDetails { Kilojoule = 903, Rating = 10 }});
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var iceCreams = context.IceCreams
.OrderBy(i => i.IceCreamId)
.ToList();
Trace.Assert(iceCreams.Count == 2);
Trace.Assert(iceCreams[0].Details.Kilojoule == 865);
Trace.Assert(iceCreams[1].Details.Rating == 10);
}
}
}
You can find the most comprehensive JSON sample code on our repository (see the JSON mapping and query scenarios section).
I'm currently working on the Asp.Net Core MVC project and have the following Message class
public class Message
{
public int Id { get; set; }
public string SenderId { get; set; }
[ForeignKey("SenderId")]
public virtual User Sender { get; set; }
public string RecipientId { get; set; }
[ForeignKey("RecipientId")]
public virtual User Recipient { get; set; }
public string Content { get; set; }
public bool IsRead { get; set; }
public DateTime? DateRead { get; set; }
public DateTime MessageSent { get; set; }
public bool SenderDeleted { get; set; }
public bool RecipientDeleted { get; set; }
}
Using the mapper, I get the below ViewModel for the Message review:
public class MessageReviewViewModel
{
public int Id { get; set; }
public string SenderId { get; set; }
[ForeignKey("SenderId")]
public virtual User Sender { get; set; }
public string RecipientId { get; set; }
[ForeignKey("RecipientId")]
public virtual User Recipient { get; set; }
public string Content { get; set; }
public bool IsRead { get; set; }
public DateTime? DateRead { get; set; }
public DateTime MessageSent { get; set; }
}
Now, I would like to get actually the Recipient and Sender users, however the reason I have such a simple MessageReviewViewModel is because I'm getting the list of this view models and with this users json throws an error & without it I have successfull result.
However, the problem is, that for the proper display in the Inbox view I still need the certain properties of Recipient and Sender user's (their main photo url, username & etc).
My mapper configuration is as below to get the messages from the repository:
public async Task<IEnumerable<MessageReviewViewModel>> GetMessagesForUserByUserId(string userId)
{
var messages = await messageRepository.GetMessagesForUser(userId);
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Message, MessageReviewViewModel>();
cfg.IgnoreUnmapped();
});
IMapper mapper = config.CreateMapper();
var messageList = mapper.Map<IEnumerable<Message>, IEnumerable<MessageReviewViewModel>>(messages);
return messageList;
}
In the react component (I have integrated react.js in the asp.net core mvc) once it did mount I make a get request to get the messages list as below and setState messagesList to the received array.
componentDidMount() {
const xhr = new XMLHttpRequest();
xhr.open('get', "/Messages/GetMessages", true);
xhr.onload = () => {
const data = JSON.parse(xhr.responseText);
this.setState({ messagesList: data });
console.log(this.state.messagesList);
};
xhr.send();
}
And this is the action in the controller, that is being called:
[HttpGet]
public async Task<IActionResult> GetMessages()
{
var userFromRepo = await userManager.FindByNameAsync(User.Identity.Name);
var messages = await messagesService.GetMessagesForUserByUserId(userFromRepo.Id);
var sortedMessageList = messages.OrderByDescending(m => m.MessageSent);
return Json(sortedMessageList);
}
As I mentioned, it all works without any problem unless there are no virtual User Sender and virtual User Recipient in the MessageReviewViewModel. Once I have them in code, this is the error I get:
Probably it is worth to mention, that the User class objects (in my scenario Sender & Recipient for example) also have the virtual properties inside and I thought that it may be problem for Json parse these objects, which have other objects as property.
Could you please advise how I can include these properties in the ViewModel so neither mapper nor JSON throw any error? I'm okay even to get only selected properties of the obje (for example just string username, mainphoto url & etc).
It is also ok, if there is any Json method, that will solve this parseError with virtual users included in the ViewModel
I figured it out after finding that mapper can also be configured to the level that is required by the user
The Message model is as below as previously
public class Message
{
public int Id { get; set; }
public string SenderId { get; set; }
[ForeignKey("SenderId")]
public virtual User Sender { get; set; }
public string RecipientId { get; set; }
[ForeignKey("RecipientId")]
public virtual User Recipient { get; set; }
public string Content { get; set; }
public bool IsRead { get; set; }
public DateTime? DateRead { get; set; }
public DateTime MessageSent { get; set; }
public bool SenderDeleted { get; set; }
public bool RecipientDeleted { get; set; }
}
And the MessageReviewViewModel changed to below:
public class MessageReviewViewModel
{
public int Id { get; set; }
public string SenderId { get; set; }
public string SenderUsername { get; set; }
public string SenderMainPhotoUrl { get; set; }
public string RecipientId { get; set; }
[ForeignKey("RecipientId")]
public string RecipientUsername { get; set; }
public string RecipientMainPhotoUrl { get; set; }
public string Content { get; set; }
public bool IsRead { get; set; }
public DateTime? DateRead { get; set; }
public DateTime MessageSent { get; set; }
}
Only for the mapper the configuration was required as below:
public async Task<IEnumerable<MessageReviewViewModel>> GetMessagesForUserByUserId(string userId)
{
var messages = await messageRepository.GetMessagesForUser(userId);
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Message, MessageReviewViewModel>()
.ForMember(destination=>destination.RecipientUsername,map=>map.MapFrom(
source=>source.Recipient.UserName)) //gets only string for username instead of whole User model
.ForMember(destination=> destination.RecipientMainPhotoUrl,map=>map.MapFrom(
source=>source.Recipient.MainProfilePicture)) //gets only string for profile picture instead of whole User model
.ForMember(destination=>destination.SenderUsername,map=>map.MapFrom(
source=>source.Sender.UserName))
.ForMember(destination=>destination.SenderMainPhotoUrl,map=>map.MapFrom(
source=>source.Sender.MainProfilePicture));
cfg.IgnoreUnmapped();
});
IMapper mapper = config.CreateMapper();
var messageList = mapper.Map<IEnumerable<Message>, IEnumerable<MessageReviewViewModel>>(messages);
return messageList;
}
As all received data now are only strings, Json has no problem parsing it
In my project there is one to may relationship with two table(questions and asnwars). but when creating question no need to add answar but in my problem getting this error
Object reference not set to an instance of an object.
how can I solve this? is there any wrong thing in my code
this is the code what I tried.
public class QuestionDTO
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
public string question { get; set; }
public int votes { get; set; }
public ApplicationUser starter { get; set; }
public virtual ICollection<VegCategoryDTO> tags { get; set; }
public ICollection<AnswerDTO> answers { get; set; }
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<QuestionDTO>().ToTable("tblQuestion");
modelBuilder.Entity<AnswerDTO>()
.HasRequired<QuestionDTO>(s => s.Question)
.WithMany(g => g.answers)
.HasForeignKey<int>(s => s.QuestionId)
.WillCascadeOnDelete();
}
}
public class QuestionController : ApiController
{
[AllowAnonymous]
public QuestionVM Post(QuestionVM model)
{
//if (!ModelState.IsValid)
//{
// return null;
//}
QuestionDTO dto = new QuestionDTO();
using (Db db = new Db())
{
dto.Id = model.Id;
dto.Title = model.Title;
dto.votes = model.votes;
dto.question = model.question;
ApplicationUser currentUser = (new ApplicationDbContext()).Users.FirstOrDefault(x => x.Id == userId);
dto.starter = currentUser;
dto.tags = model.tags;
dto.answers = model.answers;
db.Questions.Add(dto);
db.SaveChanges();
}
//_DbContext.SaveChanges();
return new QuestionVM(dto);
}
}
public class Db : DbContext
{
public DbSet<VegCategoryDTO> VegCategories { get; set; }
public DbSet<QuestionDTO> Questions { get; set; }
public DbSet<AnswerDTO> Answers { get; set; }
public DbSet<ApplicationUser> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<QuestionDTO>()
.HasMany<VegCategoryDTO>(s => s.tags)
.WithMany(c => c.Questions)
.Map(cs =>
{
cs.MapLeftKey("QuestiontRefId");
cs.MapRightKey("VegCategoryRefId");
cs.ToTable("QuestiontVegCategory");
});
modelBuilder.Entity<AnswerDTO>()
.HasRequired<QuestionDTO>(s => s.Question)
.WithMany(g => g.answers)
.HasForeignKey<int>(s => s.QuestionId)
.WillCascadeOnDelete();
}
}
Ok, looks like someone should run into this, but I have not found anything that helps. This is my scenario.
I have a clients and skills model. The objective is to allow clients to rate each of a set of skills, all of them.
So, here I'll paste model, viewModel, Controller and View. at the end the crucial question
MODEL
namespace ClientSkills.Models
{
public class Client
{
public int ClientId { get; set; }
public string Name { get; set; }
public ICollection<ClientSkills> Skills { get; set; }
}
public class Skill
{
public int SkillId { get; set; }
public string Name { get; set; }
public ICollection<ClientSkills> Clients { get; set; }
}
public class Rating
{
public int RatingId { get; set; }
public string Name { get; set; }
public ICollection<ClientSkills> ClientSkills { get; set; }
}
public class ClientSkills
{
[Key, Column(Order=0)]
public int ClientId { get; set; }
[Key, Column(Order = 1)]
public int SkillId { get; set; }
public int RatingId { get; set; }
public Rating Rating { get; set; }
public Skill Skill { get; set; }
public Client Client { get; set; }
}
public partial class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<Client> Client { get; set; }
public DbSet<Skill> Skill { get; set; }
public DbSet<Rating> Rating { get; set; }
public DbSet<ClientSkills> ClientSkills { get; set; }
}
}
VIEW MODEL
namespace ClientSkills.Models
{
public class RateViewModel
{
public RateViewModel()
{
this.Skills = new List<SkillViewModel>();
}
public RateViewModel(Client client)
{
this.ClientId = client.ClientId;
this.Skills = new List<SkillViewModel>();
if (client.Skills.Count == 0)
{
var context = new ApplicationDbContext();
foreach (var skill in context.Skill)
{
var skillVM = new SkillViewModel(skill);
skillVM.SelectedRatingid = context.Rating.First(r => r.Name == "No aplica").RatingId;
Skills.Add(skillVM);
}
}
else
{
foreach (var item in client.Skills)
{
var skillVM = new SkillViewModel(item);
skillVM.SelectedRatingid = item.SkillId;
this.Skills.Add(skillVM);
}
}
}
public int ClientId { get; set; }
public List<SkillViewModel> Skills { get; set; }
}
public class SkillViewModel
{
public SkillViewModel()
{
Ratings = new List<Rating>();
}
public SkillViewModel(Skill skill)
{
var context = new ApplicationDbContext();
this.Ratings = context.Rating.ToList();
this.SkillId = skill.SkillId;
this.SkillName = skill.Name;
}
public SkillViewModel(ClientSkills item)
{
var context = new ApplicationDbContext();
this.Ratings = context.Rating.ToList();
this.SkillId = item.SkillId;
this.SkillName = item.Skill.Name;
this.SelectedRatingid = item.RatingId;
}
public List<Rating> Ratings { get; set; }
public int SelectedRatingid { get; set; }
public int SkillId { get; set; }
public string SkillName { get; set; }
}
}
CONTROLLER
namespace ClientSkills.Controllers
{
public class RateController : Controller
{
//
// GET: /Rate/Create
public ActionResult Create()
{
//first, we assume there is an already selected user
var context = new ApplicationDbContext();
var client = context
.Client
.Include(c => c.Skills)
.First(c => c.Name.ToLower() == "ricker");
var model = new RateViewModel(client);
return View(model);
}
}
}
VIEW
This View is made creating a Create view with the wizard, deleting the useless field it creates and putting the code below
<table>
<tr>
<td>Skill</td>
<td>Rate</td>
</tr>
#foreach (var item in Model.Skills)
{
<tr>
<td>
#Html.DisplayFor(model => model.Skills.First(i => i.SkillName == item.SkillName).SkillName, new { #class = "control-label col-md-2" })
</td>
<td>
#Html.DropDownListFor(x => item.SelectedRatingid, new SelectList(item.Ratings, "RatingId", "Name"), "Seleccione el valor")
</td>
</tr>
}
</table>
The problem is that in the RateViewModel(Client client) constructor in the line skillVM.SelectedRatingid = context.Rating.First(r => r.Name == "No aplica").RatingId; I set a default value for the selectedRatingId property. I need to make sure all clients has all skills rated even if they forgot or bypass one of them.
When the view is loaded, and I was sure the ratesid was well set, the skill names look ok but the drop dpwn lists does not show the selected default value.
How can I make the view to bind the selectedRatingId property to the drop down list control?
Passing selected value to SelectList:
#Html.DropDownListFor(x => item.SelectedRatingid, new SelectList(item.Ratings, "RatingId", "Name", item.SelectedRatingid), "Seleccione el valor")
As I understand it (rightly or wrongly) that last parameter is an object if it matches with one of the options values - it is selected else the first one in the options is selected.
More info about the Selectlist overload.
Following is a save routine on existing record. What's wrong with this? I'm using independent association
There's no error emitted, however, the Country_CountryId field on Person table didn't change, everything else are properly persisted. What's wrong on the following code/approach?
public JsonResult SaveUpdate(Person p)
{
p.Country = new Country { CountryId = new Guid("EF0CD98E-7138-4757-866E-ADC3C8D216DA") };
using (var db = new TheDbContext())
{
db.Entry(p).State = System.Data.EntityState.Modified;
db.SaveChanges();
}
}
Here's my mapper:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Person>().HasRequired(x => x.Country)
.WithMany(x => x.Persons)
.Map(x => x.MapKey("Country_CountryId"));
modelBuilder.Entity<Person>().Property(x => x.RowVersion).IsRowVersion();
modelBuilder.Entity<Country>().HasKey(x => x.CountryId);
}
Here's my models:
public class Country
{
public virtual Guid CountryId { get; set; }
public virtual string CountryCode { get; set; }
public virtual string CountryName { get; set; }
public virtual IList<Person> Persons { get; set; }
}
public class Person
{
public virtual Guid PersonId { get; set; }
public virtual string Username { get; set; }
public virtual string Firstname { get; set; }
public virtual string Lastname { get; set; }
public virtual byte[] RowVersion { get; set; }
public virtual Country Country { get; set; }
}
Use this instead and it will work:
public JsonResult SaveUpdate(Person p)
{
var country = new Country { CountryId = new Guid("EF0CD98E-7138-4757-866E-ADC3C8D216DA") };
using (var db = new TheDbContext())
{
db.People.Attach(p);
db.Countries.Attach(country);
db.Entry(p).State = System.Data.EntityState.Modified;
p.Country = country;
db.SaveChanges();
}
}
Independent associations require special care because each association itself has its own state which must be configured.