Included property returns object that it's included to - entity-framework-core

I'm working on my side project with is a WebAPI in ASP.NET Core 2.1. I'm using Entity Framework Core 2.1.
I have a User:
public class User : Account
{
public UInt64 UserEmail { get; protected set; }
public UserImage UserImage { get; set; }
public virtual ICollection<Recipe> Recipes { get; set; }
public User() { }
public User(string nick, UInt64 login, byte[] salt, byte[] passwordHash,
string restoreKey, UInt64 userEmail) : base(nick, login, salt, passwordHash, restoreKey)
{
UserEmail = userEmail;
Role = "user";
}
public void Update(UInt64 login, UInt64 userEmail)
{
Login = login;
UserEmail = userEmail;
UpdatedAt = DateTime.UtcNow;
}
public void UpdatePassword(byte[] newPassword)
{
PasswordHash = newPassword;
UpdatedAt = DateTime.UtcNow;
}
}
and UserImage:
public class UserImage : Image
{
public int? UserRef { get; set; }
public virtual User User { get; set; }
public UserImage() : base() { }
public UserImage(string content) : base(content) { }
}
In my DbContext I have something like this:
modelBuilder.Entity<User>()
.HasOne(x => x.UserImage)
.WithOne(y => y.User)
.HasForeignKey<UserImage>(y => y.UserRef)
.IsRequired(false);
I have a method in my service that returns a user with his image:
public async Task<User> GetAsync(int id)
{
var user = await _context.Users.GetById(id)
.Include(x => x.UserImage)
.SingleOrDefaultAsync();
if (user == null)
throw new CorruptedOperationException("Invalid id");
return user;
}
The problem is that when I use a Postman to get a user I get that kind of response:
{
"userEmail": 2606810040825320252,
"userImage": {
"userRef": 16,
"user": {
"userEmail": 2606810040825320252,
"recipes": null,
"nick": "MadBear123",
"login": 10458175107962595193,
"salt": "Fv/S1pnpu1u0RA6RxE1wfwCmqhbkb0Fu0W2sOuFgv//PHyizyPtmuaX8OtYkCgSJPlKMGmE2qFgg2rgs70Ee9bbMU26iVhtIApqV/Zxac54P9EXBvgkAXede3YHzSPzHkvGz3WchUUDIQqHF+EmdvPT9KuYR1Djgywxh0bDbSJk=",
"passwordHash": "Yk5S3jutaKJpwSQoRH0nk2At3nYL/Wzi+8QGRFOZuByi54o+YJHuhPYRMMSG3Vmimv1UMRWe+VA8ym2xQxoEJA==",
"role": "user",
"restoreKey": "Vub!#g17#kcP",
"id": 16,
"createdAt": "2018-10-14T11:11:43.9902857",
"updatedAt": "2018-10-14T11:11:43.990384"
},
"imageContent": "/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDjZ/jPZ6rHocDfFz4iNI19b3BU+FdFjYiEho2/5A6l8Mi53Eg4bIbkj3Sx/bi0/T/2dde0CP8AaL1xdvhK+i+w3EeiQCeR4r7bA2NMj3rtnLhScEyLtJZRt+a/2fH8S/Ha5t77wn4x+Hfiqy0q8neCHRrW8vL2dTpuq3bhbeOOWbcqQNshdDNI8kaRqzAkYXiT9tDxF8DLbxlp+peE9b8davp8b6fdaB4ZsL9LqKFZJI57m9lubJhp6o6JGYZojch2w8UIwzEpSneEd9OtjOPJDV7el/yP2V+Hv7VvhvxLrhcftfaTHYl5CkWpXfhhHwJJFCsBBG4OAp6Y57cVjfAL9rHwj4Y/Zx+HMM37YXwb0KSPw7pkLWep3OjF4X+zQp5J/wBJjbcGIXB5yQOtfLn7FH/BbZpPA0eqN8N/EXgLVoI7u5utD8f6nqVrDeRxh5B/Z15baNLFdyNiSMwlIpt6/LG6q7pa0T/gtFefD79kf4e3uo6h8CdBsdA8N2S2clz8ablL7Xo47GMFhpy6FJM8i5IKhSySoACzqm6fehG1rtFq0pXvZH4L/sdftNWH7OOo61f65pt94g0W7sWsY9OsPEUmky3N23EMjT2zrcxLFG9w6tGrK0gjjcFJHFdR8ef2620zXZtY+EPi/wCLWh3HisTy+JLbxFqqXVxb3MgdHe0voPLdoZI3QFJIw6vACXkDkD550T4a61qHmC3jhZmwCd3T9K0JPgT4vZvlsvM3DGVfj9a6oymlaK179TCXI37z07dD6k+B3/BT/VPF+ixt8dvGnxw+JMvhXVDqnhfQtP8AEUVnZ3FxPb/Zp5r69niuLhsQBo4lEbGP7ROysm+RJfIf21v2pNU/a++KknjTVJLiBZ4I9IhtL3xFNq1zp0NuCY4fOu5ZLuaNVkAEk5JJ3ruYoSeb8IfslfEDxEFa2t7W3C95p9nqewNaXxW/Z48caV4b0e21OOzm/s8zKskV2824OwY/KVG0jpkZJGAeAMaSjVlG8l87av5mcalGMrRa9L6L0Wx//9k=",
"id": 10,
"createdAt": "2018-10-16T05:45:42.4644513",
"updatedAt": "2018-10-16T05:45:42.4645292"
},
"recipes": null,
"nick": "MadBear123",
"login": 10458175107962595193,
"salt": "Fv/S1pnpu1u0RA6RxE1wfwCmqhbkb0Fu0W2sOuFgv//PHyizyPtmuaX8OtYkCgSJPlKMGmE2qFgg2rgs70Ee9bbMU26iVhtIApqV/Zxac54P9EXBvgkAXede3YHzSPzHkvGz3WchUUDIQqHF+EmdvPT9KuYR1Djgywxh0bDbSJk=",
"passwordHash": "Yk5S3jutaKJpwSQoRH0nk2At3nYL/Wzi+8QGRFOZuByi54o+YJHuhPYRMMSG3Vmimv1UMRWe+VA8ym2xQxoEJA==",
"role": "user",
"restoreKey": "Vub!#g17#kcP",
"id": 16,
"createdAt": "2018-10-14T11:11:43.9902857",
"updatedAt": "2018-10-14T11:11:43.990384"
}
Like you can see I get a user with his image and in this image i get (again) this user.
I would like to get response like this:
{
"userEmail": 2606810040825320252,
"userImage": {
"userRef": 16,
"user": null,
"imageContent": "/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDjZ/jPZ6rHocDfFz4iNI19b3BU+FdFjYiEho2/5A6l8Mi53Eg4bIbkj3Sx/bi0/T/2dde0CP8AaL1xdvhK+i+w3EeiQCeR4r7bA2NMj3rtnLhScEyLtJZRt+a/2fH8S/Ha5t77wn4x+Hfiqy0q8neCHRrW8vL2dTpuq3bhbeOOWbcqQNshdDNI8kaRqzAkYXiT9tDxF8DLbxlp+peE9b8davp8b6fdaB4ZsL9LqKFZJI57m9lubJhp6o6JGYZojch2w8UIwzEpSneEd9OtjOPJDV7el/yP2V+Hv7VvhvxLrhcftfaTHYl5CkWpXfhhHwJJFCsBBG4OAp6Y57cVjfAL9rHwj4Y/Zx+HMM37YXwb0KSPw7pkLWep3OjF4X+zQp5J/wBJjbcGIXB5yQOtfLn7FH/BbZpPA0eqN8N/EXgLVoI7u5utD8f6nqVrDeRxh5B/Z15baNLFdyNiSMwlIpt6/LG6q7pa0T/gtFefD79kf4e3uo6h8CdBsdA8N2S2clz8ablL7Xo47GMFhpy6FJM8i5IKhSySoACzqm6fehG1rtFq0pXvZH4L/sdftNWH7OOo61f65pt94g0W7sWsY9OsPEUmky3N23EMjT2zrcxLFG9w6tGrK0gjjcFJHFdR8ef2620zXZtY+EPi/wCLWh3HisTy+JLbxFqqXVxb3MgdHe0voPLdoZI3QFJIw6vACXkDkD550T4a61qHmC3jhZmwCd3T9K0JPgT4vZvlsvM3DGVfj9a6oymlaK179TCXI37z07dD6k+B3/BT/VPF+ixt8dvGnxw+JMvhXVDqnhfQtP8AEUVnZ3FxPb/Zp5r69niuLhsQBo4lEbGP7ROysm+RJfIf21v2pNU/a++KknjTVJLiBZ4I9IhtL3xFNq1zp0NuCY4fOu5ZLuaNVkAEk5JJ3ruYoSeb8IfslfEDxEFa2t7W3C95p9nqewNaXxW/Z48caV4b0e21OOzm/s8zKskV2824OwY/KVG0jpkZJGAeAMaSjVlG8l87av5mcalGMrRa9L6L0Wx//9k=",
"id": 10,
"createdAt": "2018-10-16T05:45:42.4644513",
"updatedAt": "2018-10-16T05:45:42.4645292"
},
"recipes": null,
"nick": "MadBear123",
"login": 10458175107962595193,
"salt": "Fv/S1pnpu1u0RA6RxE1wfwCmqhbkb0Fu0W2sOuFgv//PHyizyPtmuaX8OtYkCgSJPlKMGmE2qFgg2rgs70Ee9bbMU26iVhtIApqV/Zxac54P9EXBvgkAXede3YHzSPzHkvGz3WchUUDIQqHF+EmdvPT9KuYR1Djgywxh0bDbSJk=",
"passwordHash": "Yk5S3jutaKJpwSQoRH0nk2At3nYL/Wzi+8QGRFOZuByi54o+YJHuhPYRMMSG3Vmimv1UMRWe+VA8ym2xQxoEJA==",
"role": "user",
"restoreKey": "Vub!#g17#kcP",
"id": 16,
"createdAt": "2018-10-14T11:11:43.9902857",
"updatedAt": "2018-10-14T11:11:43.990384"
}
Or something similar. I tried to work my way with Include() and (or without) virtual but I ended-up with this.
UPDATE
Definition for GetById():
public static IQueryable<User> GetById(this IQueryable<User> value,int id)
=> value.Where(x => x.Id == id);
You can find full project on GitHub. I'm working on branch MB#20.

For looping reference, you could not control it in EF Core.
For general handling, we configure it in Startup.cs like
.AddJsonOptions(options =>
{
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json
.ReferenceLoopHandling.Ignore;
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
});
And usecase:
var user = await _userService.GetAsync(id);
return Ok(user);
For your issue, you modified user before return by return Ok(user.ShapeData(fields)); which made JsonSerialize fail to know its the loop reference.
For a workaround, before converting to ExpandoObject, handle the loop reference like
public static ExpandoObject ShapeData<TSource>(this TSource source, string fields)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
var result =JsonConvert.SerializeObject(source, new JsonSerializerSettings{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
} );
source = JsonConvert.DeserializeObject<TSource>(result);
var dataShapedObject = new ExpandoObject();
//your rest code
return dataShapedObject;
}
Update
For null in UserImage, it is caused by you specify the properties as public string ImageContent { get; protected set; }.
Try to custom DefaultContractResolver to deserialize the protected properties.
IncludePrivateStateContractResolver.cs
public class IncludePrivateStateContractResolver : DefaultContractResolver
{
protected override List<MemberInfo> GetSerializableMembers(Type objectType)
{
const BindingFlags BindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
var properties = objectType.GetProperties(BindingFlags);//.Where(p => p.HasSetter() && p.HasGetter());
var fields = objectType.GetFields(BindingFlags);
var allMembers = properties.Cast<MemberInfo>().Union(fields);
return allMembers.ToList();
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);
if (!prop.Writable)
{
var property = member as PropertyInfo;
if (property != null)
{
prop.Writable = property.HasSetter();
}
else
{
var field = member as FieldInfo;
if (field != null)
{
prop.Writable = true;
}
}
}
if (!prop.Readable)
{
var field = member as FieldInfo;
if (field != null)
{
prop.Readable = true;
}
}
return prop;
}
}
public static class TypeExtensions
{
public static bool HasSetter(this PropertyInfo property)
{
//In this way we can check for private setters in base classes
return property.DeclaringType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
.Any(m => m.Name == "set_" + property.Name);
}
public static bool HasGetter(this PropertyInfo property)
{
//In this way we can check for private getters in base classes
return property.DeclaringType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
.Any(m => m.Name == "get_" + property.Name);
}
}
usecase:
public static ExpandoObject ShapeData<TSource>(this TSource source, string fields)
{
var soureType = source.GetType();
if (source == null)
{
throw new ArgumentNullException("source");
}
var serializeSettings = new JsonSerializerSettings{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
var deserializeSettings = new JsonSerializerSettings{
ContractResolver = new IncludePrivateStateContractResolver()
};
var result =JsonConvert.SerializeObject(source, serializeSettings );
var castType = JsonConvert.DeserializeObject<TSource>(result, deserializeSettings);
var dataShapedObject = new ExpandoObject();

I managed to fix my problem. I changed all private setters in models properties to public an used Tao Zhou original answer.
My ShapeData method:
public static ExpandoObject ShapeData<TSource>(this TSource source, string fields)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
var result = JsonConvert.SerializeObject(source, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
source = JsonConvert.DeserializeObject<TSource>(result);
var dataShapedObject = new ExpandoObject();
// rest of my code
}
User:
public class User : Account
{
public UInt64 UserEmail { get; set; }
public UserImage UserImage { get; set; }
public virtual ICollection<Recipe> Recipes { get; set; }
// rest of the code
}
UserImage:
public class UserImage : Image
{
public int? UserRef { get; set; }
public virtual User User { get; set; }
public UserImage() : base() { }
public UserImage(string content) : base(content) { }
}
Example of the returned data:
{
"userEmail": 2606810040825320252,
"userImage": {
"userRef": 16,
"user": null,
"imageContent": "/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDjZ/jPZ6rHocDfFz4iNI19b3BU+FdFjYiEho2/5A6l8Mi53Eg4bIbkj3Sx/bi0/T/2dde0CP8AaL1xdvhK+i+w3EeiQCeR4r7bA2NMj3rtnLhScEyLtJZRt+a/2fH8S/Ha5t77wn4x+Hfiqy0q8neCHRrW8vL2dTpuq3bhbeOOWbcqQNshdDNI8kaRqzAkYXiT9tDxF8DLbxlp+peE9b8davp8b6fdaB4ZsL9LqKFZJI57m9lubJhp6o6JGYZojch2w8UIwzEpSneEd9OtjOPJDV7el/yP2V+Hv7VvhvxLrhcftfaTHYl5CkWpXfhhHwJJFCsBBG4OAp6Y57cVjfAL9rHwj4Y/Zx+HMM37YXwb0KSPw7pkLWep3OjF4X+zQp5J/wBJjbcGIXB5yQOtfLn7FH/BbZpPA0eqN8N/EXgLVoI7u5utD8f6nqVrDeRxh5B/Z15baNLFdyNiSMwlIpt6/LG6q7pa0T/gtFefD79kf4e3uo6h8CdBsdA8N2S2clz8ablL7Xo47GMFhpy6FJM8i5IKhSySoACzqm6fehG1rtFq0pXvZH4L/sdftNWH7OOo61f65pt94g0W7sWsY9OsPEUmky3N23EMjT2zrcxLFG9w6tGrK0gjjcFJHFdR8ef2620zXZtY+EPi/wCLWh3HisTy+JLbxFqqXVxb3MgdHe0voPLdoZI3QFJIw6vACXkDkD550T4a61qHmC3jhZmwCd3T9K0JPgT4vZvlsvM3DGVfj9a6oymlaK179TCXI37z07dD6k+B3/BT/VPF+ixt8dvGnxw+JMvhXVDqnhfQtP8AEUVnZ3FxPb/Zp5r69niuLhsQBo4lEbGP7ROysm+RJfIf21v2pNU/a++KknjTVJLiBZ4I9IhtL3xFNq1zp0NuCY4fOu5ZLuaNVkAEk5JJ3ruYoSeb8IfslfEDxEFa2t7W3C95p9nqewNaXxW/Z48caV4b0e21OOzm/s8zKskV2824OwY/KVG0jpkZJGAeAMaSjVlG8l87av5mcalGMrRa9L6L0Wx//9k=",
"id": 10,
"createdAt": "2018-10-16T05:45:42.4644513",
"updatedAt": "2018-10-16T05:45:42.4645292"
},
"recipes": null,
"nick": "MadBear123",
"login": 10458175107962595193,
"salt": "Fv/S1pnpu1u0RA6RxE1wfwCmqhbkb0Fu0W2sOuFgv//PHyizyPtmuaX8OtYkCgSJPlKMGmE2qFgg2rgs70Ee9bbMU26iVhtIApqV/Zxac54P9EXBvgkAXede3YHzSPzHkvGz3WchUUDIQqHF+EmdvPT9KuYR1Djgywxh0bDbSJk=",
"passwordHash": "Yk5S3jutaKJpwSQoRH0nk2At3nYL/Wzi+8QGRFOZuByi54o+YJHuhPYRMMSG3Vmimv1UMRWe+VA8ym2xQxoEJA==",
"role": "user",
"restoreKey": "Vub!#g17#kcP",
"id": 16,
"createdAt": "2018-10-14T11:11:43.9902857",
"updatedAt": "2018-10-14T11:11:43.990384"
}

Related

How to return object references in ASP.net core Web API

I dont understand why getAllAssemblies() return referenced objects where "getAssembly(int id)" does not.
Can you give a heads up on how to have referenced objects returned from both methods?
The type:
public class Assembly
{
public int Id { get; init; }
public string? Name { get; set; }
public List<Object>? ChildrenObjects { get; set; }
[Required]
public int ParrentObjectId { get; set; }
[JsonIgnore]
public Assembly? ParrentObject { get; set; }
}
The data:
modelBuilder.Entity<Assembly>().HasData(
new Assembly() { Id = 10, Name = "1" },
new Assembly() { Id = 11, Name = "1.1", ParrentObjectId = 10
});
GetAssembly(int id)
[HttpGet("{id}")]
public async Task<ActionResult> GetAssembly(int id)
{
return Ok(await _objectsContext.Assemblies.FindAsync(id));
}
returns:
{
"id": 10,
"name": "1",
"childrenObjects": null,
"parrentObjectId": 0
}
GetAllAssemblies()
public async Task<ActionResult> GetAllAssemblies()
{
return Ok(await _objectsContext.Assemblies.ToListAsync());
}
returns:
[
{
"id": 10,
"name": "1",
"childrenObjects": [
{
"id": 11,
"name": "1.1",
"childrenObjects": null,
"parrentObjectId": 10
}
],
"parrentObjectId": 0
},
{
"id": 11,
"name": "1.1",
"childrenObjects": null,
"parrentObjectId": 10
}
]

Why my EF6 generated relationship not working?

I have two tables (Jobs and Versions) on a One to Many relationship in a PostgreSQL database. I have generated a DB Context and Models with dotnet-ef (EF6 Database-first) and created routes with JsonApiDotNetCore.
My two models :
// A Version (with one job)
[DisplayName("version")]
[Table("Versions")]
public partial class Version : Identifiable<long>
{
[Attr(PublicName = "id-version")]
public override long Id { get; set; }
[Attr(PublicName = "id-job")]
public long JobId { get; set; }
[Attr(PublicName = "name")]
public string Name { get; set; }
[Attr(PublicName = "job")]
public virtual Job JobIdNavigation { get; set; }
}
// A Job (with multiple Versions)
[DisplayName("job")]
[Table("Jobs")]
public partial class Job : Identifiable<long>
{
public Job()
{
this.Versions = new HashSet<Version>();
}
[Attr(PublicName = "id-job")]
public override long Id { get; set; }
[Attr(PublicName = "name")]
public string Name { get; set; }
[Attr(PublicName = "versions")]
public virtual ICollection<Version> Versions { get; set; }
}
And a DB Context :
public partial class TalendExplorerDbContext : DbContext
{
[...]
public virtual DbSet<Job> Jobs { get; set; }
public virtual DbSet<Version> Versions { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("adminpack")
.HasPostgresExtension("ltree")
.HasAnnotation("Relational:Collation", "French_France.1252");
modelBuilder.Entity<Job>(entity =>
{
entity.HasKey(e => e.Id)
.HasName("Jobs_pkey");
entity.Property(e => e.Id)
.HasColumnName("job_id")
.UseIdentityAlwaysColumn();
[...]
});
modelBuilder.Entity<Version>(entity =>
{
entity.HasKey(e => e.Id)
.HasName("Versions_pkey");
entity.Property(e => e.Id)
.HasColumnName("version_id")
.UseIdentityAlwaysColumn();
entity.Property(e => e.JobId).HasColumnName("job_id");
[...]
entity.HasOne<Job>(d => d.JobIdNavigation)
.WithMany(p => p.Versions)
.HasForeignKey(d => d.JobId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("Versions_id_job_fkey");
});
OnModelCreatingPartial(modelBuilder);
}
[...]
}
With those, I can get a resource like a Job but there is no Versions linked:
{
"links": {
"self": "https://localhost:5001/jobs/2"
},
"data": {
"type": "jobs",
"id": "2",
"attributes": {
"name": "job2",
"versions": []
},
"links": {
"self": "https://localhost:5001/jobs/2"
}
}
}
Is this normal behavior ? How can I make my relationship work ?
Edit 1
As suggest by Michael Mairegger, I try to include the relationship on the request:
from https://localhost:5001/jobs/2
to https://localhost:5001/jobs/2?include=versions
but got an error:
{
"errors": [
{
"id": "4c6d79c9-0af7-419b-b89c-d3a61588b73a",
"status": "400",
"title": "The specified include is invalid.",
"detail": "Relationship 'versions' does not exist on resource 'jobs'.",
"source": { "parameter": "include" }
}
]
}
I am not familiar with JsonApiDotNetCore but I think the reason is the same as in ODATA. The API does not load related data because you did not request it. Otherwise it can happen that you accidently load the whole database because every data is somehow connected to any other data.
The API supports an include query parameter where you can request the additional navigation properties. I think if you execute https://localhost:5001/jobs/2?include=versions the versions shall be in the results set.
See: Including Relationships for further information.
As suggest by a co-worker (and Michael Mairegger), I had to specify relationship with JsonApiDotNetCore.
So I need to change annotations on models:
// A Version (with one job)
[DisplayName("version")]
[Table("Versions")]
public partial class Version : Identifiable<long>
{
[...]
[HasOne(PublicName = "job")]
public virtual Job JobIdNavigation { get; set; }
}
// A Job (with multiple Versions)
[DisplayName("job")]
[Table("Jobs")]
public partial class Job : Identifiable<long>
{
[...]
[HasMany(PublicName = "versions")]
public virtual ICollection<Version> Versions { get; set; }
}
Now, the request https://localhost:5001/jobs/2 show a version (relationship is working! hooray!):
{
"links": {
"self": "https://localhost:5001/jobs/2"
},
"data": {
"type": "jobs",
"id": "2",
"attributes": {
"inserted-date": "2021-07-07T00:00:00+02:00",
"modification-date": null,
"path": "test",
"purpose": "purpose",
"description": "job de test",
"name": "job1",
"description-capture": null,
"purpose-capture": null
},
"relationships": {
"versions": {
"links": {
"self": "https://localhost:5001/jobs/2/relationships/versions",
"related": "https://localhost:5001/jobs/2/versions"
}
}
},
"links": {
"self": "https://localhost:5001/jobs/2"
}
}
}
Refer to Michael Mairegger anwser to get the content of related items via include keyword.

EF Core 2.2 - can't add new record with link to already existing another one

My POCO classes:
[Table]
public class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<CategoryProduct> CategoryProducts { get; set; }
}
public class CategoryProduct
{
public int CategoryId { get; set; }
public int ProductId { get; set; }
public Category Category { get; set; }
public Product Product { get; set; }
}
[Table]
public class Category
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<CategoryProduct> CategoryProducts { get; set; }
}
Here's a function for inserting new records:
async Task CreateProduct(Product dto)
{
await ctx.Products.AddAsync(dto);
await ctx.SaveChangesAsync();
}
In dto I pass the following JSON:
{
"name": "Gräff Stettin",
"categoryProducts": [{
"categoryId": 1,
"productId": 1,
"category": {
"id": 1,
"name": "Drinks"
}
}, {
"categoryId": 2,
"productId": 1,
"category": {
"id": 2,
"name": "Alcohol"
}
}
]
}
As a result, at SaveChangesAsync() I get an exception with message regarding attempt to insert already existing Category. Tracing shows the following query:
INSERT INTO "Category" ("Id", "Name") VALUES (#p0, #p1);
How should I change my CreateProduct() method to avoid attempts to add categories with already existing categoryId?
you can use AutoMapper and ignore category =
var config = new MapperConfiguration(cfg => cfg.CreateMap<categoryProductsDto,categoryProducts>())
.ForMember(u => u.Category, options => options.Ignore());
and use mapper =
var mapper = config.CreateMapper();
categoryProducts entity = mapper.Map<categoryProducts>(input);
await _categoryProductsRepository.InsertAndGetIdAsync(entity);

Eager loading for one to one relationship in EF Core

Does anyone know if is there the problem with one to one relation in EF Core and Eager Loading or the problem is my poorly knowledge instead?
For example, if I have one simple one to one relation like this:
public class Author
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Biography Biography { get; set; }
}
public class Biography
{
public int Id { get; set; }
public string BiographyResume { get; set; }
public DateTime DateOfBirth { get; set; }
public string PlaceOfBirth { get; set; }
public string Nationality { get; set; }
public int AuthorId { get; set; }
public Author Author { get; set; }
}
public class OneDbContext : DbContext
{
public DbSet<Author> Authors { get; set; }
public OneDbContext(DbContextOptions<OneDbContext> options)
: base(options)
{
}
}
Here is the Migration:
public partial class CreateAuthorsTable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Authors",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
FirstName = table.Column<string>(nullable: true),
LastName = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Authors", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Biography",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
BiographyResume = table.Column<string>(nullable: true),
DateOfBirth = table.Column<DateTime>(nullable: false),
PlaceOfBirth = table.Column<string>(nullable: true),
Nationality = table.Column<string>(nullable: true),
AuthorId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Biography", x => x.Id);
table.ForeignKey(
name: "FK_Biography_Authors_AuthorId",
column: x => x.AuthorId,
principalTable: "Authors",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Biography_AuthorId",
table: "Biography",
column: "AuthorId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Biography");
migrationBuilder.DropTable(
name: "Authors");
}
}
And now, in Controller everything is fine without Eager Loading:
[Route("api/[controller]")]
public class AuthorsController : Controller
{
private readonly OneDbContext context;
public AuthorsController(OneDbContext context)
{
this.context = context;
}
[HttpGet]
public async Task<IEnumerable<Author>> GetAuthors()
{
return await context.Authors.ToListAsync();
}
}
But when I use Eager Loading it doesn't return anything.
[HttpGet]
public async Task<IEnumerable<Author>> GetAuthors()
{
return await context.Authors.Include(a => a.Biography).ToListAsync();
}
Tested with Postman:
https://localhost:44355/api/authors
for: return await context.Authors.ToListAsync();
It returns:
[
{
"id": 1,
"firstName": "Luka",
"lastName": "Jovic",
"biography": null
},
{
"id": 2,
"firstName": "Stefan",
"lastName": "Savic",
"biography": null
}
]
for: return await context.Authors.Include(a => a.Biography).ToListAsync();
It returns:
Could not get any response
There was an error connecting to https://localhost:44355/api/authors.
Why this might have happened:
The server couldn't send a response:
Ensure that the backend is working properly
Self-signed SSL certificates are being blocked:
Fix this by turning off 'SSL certificate verification' in Settings > General
Proxy configured incorrectly
Ensure that proxy is configured correctly in Settings > Proxy
Request timeout:
Change request timeout in Settings > General

Add a reference to another document in MongoDB C# MongoDB.Driver

I have two classes:
public class MDBProducts
{
public string _id { get; set; }
public string order_number { get; set; }
[BsonIgnoreIfNull]
public List<Classes.MDBParts[]> partsId { get; set; }
}
public class MDBParts
{
public string _id { get; set; }
public string accountCode { get; set; }
}
With this lookup command mongodb embedded whole document information.
string param = "{$lookup: { from: 'Parts',localField: 'order_number',foreignField: 'accountCode',as:'partsId'} }";
BsonDocument document = BsonDocument.Parse(param);
var pipeline = new[] { document };
var result = Classes.MdB.connectDbProducts().Aggregate<MDBProducts>(pipeline).ToList();
I want to add to "partsId" only MDBParts class _id field. May you help me notice, how to do that?
References between Parts and Products collections
`var lookup1 = new BsonDocument
{
{
"$lookup",
new BsonDocument
{
{ "from", "Parts" },
{ "localField", "order_number" },
{ "foreignField", "productNumber" },
{ "as", "partsId" }
}
},
};
var lookup2 = new BsonDocument
{
{
"$project",
new BsonDocument
{
{ "partsId", "$partsId._id" },
{ "id", 1 }
}
}
};
var pipeline = new[] { lookup1, lookup2 };
var result = connectDbProducts().Aggregate<Products>(pipeline).ToList();
foreach (var item in result)
{
if (item.partsId.Count != 0)
{
var filterID = Builders<Products>.Filter.Eq(x => x._id, item._id);
var setTableTop = Builders<Products>.Update.Set(x => x.partsId, item.partsId);
connectDbProducts().UpdateOne(filterID, setTableTop);
}
else
{
var filterID = Builders<Products>.Filter.Eq(x => x._id, item._id);
var setTableTop = Builders<Products>.Update.Unset(x => x.partsId);
connectDbProducts().UpdateOne(filterID, setTableTop);
}
}`
Products Class
public class Products
{
public object _id { get; set; }
public string electricity_based_on { get; set; }
public double? full_price { get; set; }
public DateTime manufacturing_date { get; set; }
public string order_code { get; set; }
public string order_comment_names { get; set; }
public string order_name { get; set; }
public string order_number { get; set; }
public double? order_quantity { get; set; }
public double? unit_price { get; set; }
[BsonIgnoreIfNull]
public string user_name { get; set; }
[BsonIgnoreIfNull]
public DateTime user_checkTime { get; set; }
[BsonIgnoreIfNull]
public object order_comments { get; set; }
[BsonIgnoreIfNull]
public string tabletop_letter { get; set; }
[BsonIgnoreIfNull]
public string sub_3_4 { get; set; }
[BsonIgnoreIfNull]
public List<string> partsId { get; set; }
}
Connection to DB
public static IMongoCollection<Products> connectDbProducts()
{
try
{
MongoClient dbClient = new MongoClient(Properties.Resource1.mongoDB);
var db = dbClient.GetDatabase("Manufacturing");
var collection = db.GetCollection<Products>("Products");
return collection;
}
catch (Exception toMongoDBParts)
{
WriteLogFile("--MongoDB connection to Parts collection--", toMongoDBParts.StackTrace, toMongoDBParts.Message);
throw;
}
}