Automapper inner model attribute returns null - c#

so I have an entity model using Entity Framework Core which is used in ASP.NET Core Web API.
I have created two entity models:
public class FishCategory
{
[Key]
[Required]
public int ID { get; set; }
[Required]
[MaxLength(50)]
public string Category { get; set; }
public IEnumerable<Fish> Fish { get; set; }
}
public class Fish
{
[Key]
public int ID { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[MaxLength(100)]
public string Description { get; set; }
[Required]
public float WeightKg { get; set; }
[Required]
public int Stock { get; set; }
[Required]
public float Price { get; set; }
[Required]
public int CategoryID { get; set; }
public FishCategory Category { get; set; }
public int UserID { get; set; }
public User Seller { get; set; }
public Item Item { get; set; }
}
These 2 entities have one to many relationship
Then here is the 2 DTO models for both of the entities
public class FishReadDTO
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public float WeightKg { get; set; }
public int Stock { get; set; }
public float Price { get; set; }
public FishCategoryReadDTO Category { get; set; }
public UserReadDTO Seller { get; set; }
}
public class FishCategoryReadDTO
{
public int ID { get; set; }
public string Category { get; set; }
}
Here the repository for the Fish entity model
public Fish GetFishById(int id)
{
return _databaseContext.Fishes
.Where(fish => fish.ID.Equals(id))
.Select(fish => new Fish()
{
ID = fish.ID,
Category = fish.Category,
CategoryID = fish.CategoryID,
Description = fish.Description,
Item = fish.Item,
Name = fish.Name,
Price = fish.Price,
Seller = fish.Seller,
Stock = fish.Stock,
UserID = fish.UserID,
WeightKg = fish.WeightKg
}).FirstOrDefault();
}
Here's the Profile class for the DTO
public class EFishingProfile : Profile
{
public EFishingProfile()
{
CreateMap<User, UserReadDTO>();
CreateMap<FishCategory, FishCategoryReadDTO>();
CreateMap<Fish, FishReadDTO>();
CreateMap<FishCreateDTO, Fish>();
}
}
Here's the controller that executes this method to get the data
[HttpGet("{id}", Name = "GetFishById")]
public ActionResult<FishReadDTO> GetFishById(int id)
{
var fish = _repository.GetFishById(id);
if(fish == null)
{
return NotFound();
}
return Ok(_mapper.Map<FishReadDTO>(fish));
}
[HttpPost]
public ActionResult<Fish> CreateFish(FishCreateDTO fishCreateDTO)
{
var fishModel = _mapper.Map<Fish>(fishCreateDTO);
_repository.CreateFish(fishModel);
var fishRead = _mapper.Map<FishReadDTO>(fishModel);
return CreatedAtRoute(nameof(GetFishById), new {Id = fishRead.ID}, fishRead);
}
The result of posting a "Fish" data returns me a "Fish" entity model mapped to the "FishReadDTO" model but I would like to map it to the FishReadDTO but it return me a null value for property of "category" and "fish" in the Fish DTO model itself. Here is an example of it
{
"id": 12,
"name": "Test 3",
"description": "Test 3",
"weightKg": 76.25,
"stock": 10,
"price": 50.0,
"category": null,
"seller": null
}
Here's what I want which uses the FishDTO when getting a single "Fish" entity model mapped to the DTO which is working via the API method called "GetFishById(int id)". You can refer the method in my controller code posted.
{
"id": 9,
"name": "Northern Pike",
"description": "A predatorial fish found in the waters of canal and rivers",
"weightKg": 76.25,
"stock": 10,
"price": 50.0,
"category": {
"id": 1,
"category": "Salt Water/Sea Fish"
},
"seller": {
"id": 1,
"username": "AdrianJ",
"name": "Adrian Joseph",
"email": "adrianjoseph#efishing.com"
}
}
Is there any way to basically get the "category" and "seller" being mapped and not getting null?
**Note that while using the "GetByFishId(int id)" API method which invoked my "FishRepository", without using the ".Include(...)" method in the repository I still get the values for "category" and "seller", the issue is at the API method where I create/post a new Fish which is this method"
[HttpPost]
public ActionResult<Fish> CreateFish(FishCreateDTO fishCreateDTO)
" into my database and it returns me a 201 Created using "CreatedAtRoute" is when I received the response of a "FishReadDTO" with null values of "category" and "seller" only.**

You need to fetch relations from the database. When you load an entity from the DbContext, its relations are not loaded, so you get null.
In your repository, if you change
_databaseContext.Fishes
.Where(fish => fish.ID.Equals(id))
to
_databaseContext.Fishes
.Include(e => e.Category)
.Include(e => e.Seller)
.FirstOrDefault(fish => fish.ID.Equals(id))
then DbContext will load those relations and navigation properties Category and Seller will be populated. Then AutoMapper can map those objects to DTOs like you expect.
For reference, see docs

Related

How to map json response to the model with different field names

I am using an ASP.NET Core 6 and System.Text.Json library.
For example, I'm getting a response from the some API with the following structure
{
"items":
[
{
"A": 1,
"User":
{
"Name": "John",
"Age": 21,
"Adress": "some str"
},
},
{
"A": 2,
"User":
{
"Name": "Alex",
"Age": 22,
"Adress": "some str2"
},
}
]
}
And I want to write this response to the model like List<SomeEntity>, where SomeEntity is
public class SomeEntity
{
public int MyA { get; set; } // map to A
public User MyUser { get; set; } // map to User
}
public class User
{
public string Name { get; set; }
public string MyAge { get; set; } // map to Age
}
How could I do it?
UPDATE:
Is it possible to map nested properties?
public class SomeEntity
{
// should I add an attribute [JsonPropertyName("User:Name")] ?
public string UserName{ get; set; } // map to User.Name
}
Use the JsonPropertyName attribute
public class Model
{
[JsonPropertyName("items")]
public SomeEntity[] Items { get; set; }
}
public class SomeEntity
{
[JsonPropertyName("A")]
public int MyA { get; set; } // map to A
[JsonPropertyName("User")]
public User MyUser { get; set; } // map to User
}
public class User
{
public string Name { get; set; }
[JsonPropertyName("Age")]
public string MyAge { get; set; } // map to Age
}
You can then deserialize it with something like
JsonSerializer.Deserialize<Model>(response);
try this please
[JsonConverter(typeof(JsonPathConverter))]
public class SomeEntity
{
[JsonProperty("items.User.Name")]
public string UserName{ get; set; } // map to User.Name
}
deserialize using :
JsonSerializer.Deserialize<SomeEntity>(response);

Custom deserialization with System.Text.Json - Grouping fields into object

Using System.Text.Json and .NET Core 3.1, how can I deserialize the following JSON for a membership:
{
"id": 123,
"firstName": "James",
"lastName": "Smith",
"group": "Premium"
"state": "Active"
}
Classes:
public class Membership
{
public Member Member { get; set; }
public string Group { get; set; }
public string State { get; set; }
}
and
public class Member
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Thanks!
Personally, I would create a POCO / Model to match the Json exactly as it is brought in and deserialize to that. Then add a constructor to your Membership class that accepts the incoming, deserialized json model and builds up your object as desired.
It's an extra step in between getting the json and returning your own model, but since the source (incoming json) doesn't structurally match the destination (your poco object), translation has to happen somewhere. I find it easiest to follow the translation when there is this explicit separation in my code.
Something like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
public class Program
{
// I wrapped in [] to make a list
const string inJson = #"[{
""id"": 123,
""firstName"": ""James"",
""lastName"": ""Smith"",
""group"": ""Premium"",
""state"": ""Active""
}]";
public static void Main()
{
var deserialized = JsonSerializer.Deserialize<List<JsonMember>>(inJson);
var asMembership = deserialized.Select(i => new Membership(i)).ToList();
foreach(var m in asMembership){
Console.WriteLine($"Group: {m.Group}");
Console.WriteLine($"State: {m.State}");
Console.WriteLine($"Member Id: {m.Member.Id}");
Console.WriteLine($"Member First Name: {m.Member.FirstName}");
Console.WriteLine($"Member Last Name: {m.Member.LastName}");
}
}
}
public class JsonMember{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("firstName")]
public string FirstName { get; set; }
[JsonPropertyName("lastName")]
public string LastName { get; set; }
[JsonPropertyName("group")]
public string Group { get; set; }
[JsonPropertyName("state")]
public string State { get; set; }
}
public class Membership
{
public Member Member { get; set; }
public string Group { get; set; }
public string State { get; set; }
public Membership(JsonMember jsonMember){
Group = jsonMember.Group;
State = jsonMember.State;
Member = new Member{
Id = jsonMember.Id,
FirstName = jsonMember.FirstName,
LastName = jsonMember.LastName
};
}
}
public class Member
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
output:
Group: Premium
Group: Active
Member Id: 123
Member First Name: James
Member Last Name: Smith
See:
https://dotnetfiddle.net/y0i6Sx
fix your json by adding coma after "Premium"
"group": "Premium",
and try this
var json=...your json
var jD= JsonSerializer.Deserialize<Root>(json);
var memberShip = new Membership
{
Member = new Member { FirstName = jD.firstName, LastName = jD.lastName},
Group=jD.group,
State=jD.state
};
var output= JsonSerializer.Serialize(memberShip);
output
{
"Member": {
"Id": 0,
"FirstName": "James",
"LastName": "Smith"
},
"Group": "Premium",
"State": "Active"
}
Root class
public class Root
{
public int id { get; set; }
public string firstName { get; set; }
public string lastName { get; set; }
public string group { get; set; }
public string state { get; set; }
}

How to map a nested json object to a POCO with multiple custom classes using AutoMapper?

I tried several answered questions related to my problem but I can't seem to resolve the issue.
Here is the JSON object I am sending to my API:
{
"userName": "Test_06",
"password": "#00a00B00c",
"firstName": "Test",
"lastName": "Test",
"address": {
"houseNumber": 1,
"appartementBus": "A bus 34",
"street": "Teststreet",
"ZIP": "0000",
"country": "Test"
}
}
And I am trying to map the address object into the Address POCO:
using System.Collections.Generic;
namespace Kubex.Models
{
public class Address
{
public int Id { get; set; }
public int HouseNumber { get; set; }
public string AppartementBus { get; set; }
public int StreetId { get; set; }
public virtual Street Street { get; set; }
public int ZIPId { get; set; }
public virtual ZIP ZIP { get; set; }
public int CountryId { get; set; }
public virtual Country Country { get; set; }
public virtual ICollection<User> Users { get; set; }
public virtual ICollection<Company> Companies { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
}
The following types are basically the same, example of Country looks like
using System.Collections.Generic;
namespace Kubex.Models
{
public class Country
{
public byte Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
}
}
The error I am getting back from Postman is the following:
{
"errors": {
"address.ZIP": [
"Error converting value \"0000\" to type 'Kubex.Models.ZIP'. Path 'address.ZIP', line 10, position 18."
],
"address.street": [
"Error converting value \"Teststreet\" to type 'Kubex.Models.Street'. Path 'address.street', line 9, position 27."
],
"address.country": [
"Error converting value \"Test\" to type 'Kubex.Models.Country'. Path 'address.country', line 11, position 22."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|cca8afa7-463e762ee0ed670d."
}
I tried to create mappings for every Type, from string to Type. but I keep getting the errors.
Looking like this: Also tried ConstructUsing.
CreateMap<string, Country>()
.ConvertUsing(s => new Country { Name = s });
CreateMap<string, ZIP>()
.ConvertUsing(s => new ZIP { Code = s });
CreateMap<string, Street>()
.ConvertUsing(s => new Street { Name = s });
Do I need to create a map for Address too? If so, how should I do that because I don't know how it sees the object, and how I should create a map for it.
Many thanks in advance.
EDIT:
This is part of the DTO I am sending up the API endpoint to register a user,
This is the mapping used:
CreateMap<UserRegisterDTO, User>();
And this is how the DTO looks like:
using Kubex.Models;
namespace Kubex.DTO
{
public class UserRegisterDTO
{
public string UserName { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
}
This is how the User class looks like:
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
namespace Kubex.Models
{
public class User : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmployeeNumber { get; set; }
public int? AddressId { get; set; }
public virtual Address Address { get; set; }
public virtual ICollection<License> Licenses { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
}
In my service, I use this line to do the mapping:
var newUser = _mapper.Map<User>(dto);
If I understand correctly, Address class used in UserRegisterDTO is the same as in User. Not saying that this is a directly an issue, but from architecture standpoint it is, as you're mixing your DTO with database model (I guess), DTO should reference only simple types or other DTO's.
Hence, I would rather have something like this:
public class UserRegisterDTO
{
public string UserName { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public AddressDTO Address { get; set; }
}
public class AddressDTO {
public string HouseNumber { get;set; }
public string AppartementBus { get;set; }
public string Street { get;set; }
public string ZIP { get;set; }
public string Country { get;set; }
}
and may be then mapping from AddressDTO to Address:
CreateMap<AddressDTO, Address>()
.ForMember(d => d.Country, d => d.MapFrom(s => new Country { Name = s.Country }))
// cut for brevity
But I still don't like this. As this mapping would create country instance with each request and depending on your setup it might even create new row in database. May be better for it would be to lookup country within existing ones?
You can pass list of country instances through mapping context and use in ResolveUsing, but I personally prefer not to have complex logic in AutoMapper mappings, as that scatters business logic in at least two places and complicates unit testing.
Automapper is great for doing simple things, but if you feel that it is pushed to its limits, it is better to do at least some parts of the mapping manually in my opinion.

EF Core Update Linked Object

I am trying to update a company and its contact details as in the example JSON request using EF Core
PUT https://localhost:6001/api/Company
{
"CompanyId": 6,
"Name": "string",
"ContactDetail": {
"Name": "string",
"EmailAddress": "string",
"Address1": "string",
"Address2": "string",
"Address3": "string",
"PostCode": "string",
"Telephone": "string"
}
}
The generated EF Scaffold looks like this
public partial class Company
{
public long CompanyId { get; set; }
public string Name { get; set; }
public virtual ContactDetail ContactDetail { get; set; }
}
public partial class ContactDetail
{
public ContactDetail()
{
Company = new HashSet<Company>();
}
public long ContactDetailId { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string Address3 { get; set; }
public string PostCode { get; set; }
public string Telephone { get; set; }
public virtual ICollection<Company> Company { get; set; }
}
How can I update the ContactDetail in the cleanest way possible, if I was to provide the ContactDetailId then I presume it would all work fine, but ideally I only want to provide the CompanyId of the parent object.
The following code updates the Company but creates a new entry for the ContactDetail!?
public Copmany Update(Copmany company)
{
_bigLoaderContext.Update(company);
_bigLoaderContext.SaveChanges();
return GetById(company.CompanyId);
}
Update - A solution I have now used is to get the ContactDetail id and then reassign it to the object.
public Company Update(Company company)
{
var currentCompany = GetById(companyId, true);
// Update the contact detail id so that it know what one to update
company.ContactDetail.ContactDetailId = currentCompany.ContactDetailId;
_bigLoaderContext.Update(company);
_bigLoaderContext.SaveChanges();
}
I was getting an error loading the object to get the value, so had to get it with the AsNoTracking in the GetById method.
_bigLoaderContext.Company.AsNoTracking().FirstOrDefault(f => f.CompanyId == companyId);
A solution I have now used is to get the ContactDetail id and then reassign it to the object.
public Company Update(Company company)
{
var currentCompany = GetById(companyId, true);
// Update the contact detail id so that it know what one to update
company.ContactDetail.ContactDetailId = currentCompany.ContactDetailId;
_bigLoaderContext.Update(company);
_bigLoaderContext.SaveChanges();
}
I was getting an error loading the object to get the value, so had to get it with the AsNoTracking in the GetById method.
_bigLoaderContext.Company.AsNoTracking().FirstOrDefault(f => f.CompanyId == companyId);
Presume this is ok as it works!?

Pass Nested Deserialized JSON from Controller to View

After using HttpClient class to convert my JSON to a string and deserialize it with
var response = Res.Content.ReadAsStringAsync().Result;
data = JsonConvert.DeserializeObject<List<Employee>>(response);
How do I pass the data that I receive in the Controller from the call using the Model below to the View?
public class RuleType
{
public int Id { get; set; }
public string Description { get; set; }
public bool Inactive { get; set; }
}
public class RuleCategory
{
public int Id { get; set; }
public string Description { get; set; }
public bool Inactive { get; set; }
}
public class Employee
{
public string Description { get; set; }
public object EndDateTime { get; set; }
public int Id { get; set; }
public bool Inactive { get; set; }
public int RuleAction { get; set; }
public DateTime StartDateTime { get; set; }
public RuleType RuleType { get; set; }
public RuleCategory RuleCategory { get; set; }
}
Here is one object from the call
[
{
"Description": "Test Description",
"EndDateTime": null,
"Id": 1,
"Inactive": false,
"RuleAction": -2,
"StartDateTime": "2017-01-06T14:58:58Z",
"RuleType": {
"Id": 6,
"Description": "Test Description",
"Inactive": false
},
"RuleCategory": {
"Id": 1,
"Description": "Description",
"Inactive": false
}
}
]
Not sure if I'm missing something, but if you have an object you want to return to the view from the controller, you simply:
return View(viewModel); // in your case viewModel = 'data'
As others have said here already, you should be deserializing the JSON into a RootObject instead of an Employee like so:
var response = Res.Content.ReadAsStringAsync().Result;
var data = JsonConvert.DeserializeObject<List<RootObject>>(response);
You can then pass the model into the view using just:
return View(data)
You should also consider renaming RootObject into something more useful (such as employee?) as RootObject is not a very useful or descriptive name.

Categories

Resources