Missing Mapping Model after Adapting to another model - c#

I have 2 service models and 2 DAL models. While creating new Author, I want to save its books to Book table. So, I send payload as json. However, If I try to adapt model to Book model, its values are null.So I can solve this problem. I also tried model.Adapt<IEnumerable<Book>>(), this also comes null.
public async Task<AuthorRequest> CreateAsync(AuthorRequest model)
{
var authorEntity= model.Adapt<Author>(); // works fine
var bookEntity =model.Adapt<Book>();//null
}
Service.Models
public class AuthorRequest :Identifiable<string>
{
public string override Id{get;set;}
[Attr("name")]
public string Name { get; set; }
[Attr("surname")]
public string Surname { get; set; }
public ICollection<BookRequest> Books{get; set; }
}
public class BookRequest :Identifiable<string>
{
public string override Id{get;set;}
public string Title { get; set; }
}
DAL.Model
public class Author : AuditableEntity
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("surname")]
public string Surname { get; set; }
[JsonProperty("books")]
public Relationship<IList<Book>> Books;
}
public class Book :AuditableEntity
{
[JsonProperty("Title")]
public string Title { get; set; }
[JsonProperty("Author")]
public Relationship<IList<Author>> Author;
}
Mappings with mapster
TypeAdapterConfig<DAL.Models.Author, Service.Models.AuthorRequest>.NewConfig();
TypeAdapterConfig<DAL.Models.Book, Service.Models.AuthorRequest>.NewConfig();
TypeAdapterConfig<DAL.Models.Book, Service.Models.BookRequest>.NewConfig();
TypeAdapterConfig<Service.Models.AuthorRequest, DAL.Models.Author>.NewConfig();
TypeAdapterConfig<Service.Models.AuthorRequest, DAL.Models.Book>.NewConfig();
TypeAdapterConfig<Service.Models.BookRequest, DAL.Models.Book>.NewConfig();
AuthorRequest.JSON
{
"name": "William",
"surname": "Shakespeare",
"books": [{
"title": "Macheth"
}]
}

Why do you expect model.Adapt<Book>() to yield any result here? Mapping entities usually works by copying over similar properties. This works for AuthorRequest and Author because they both contain the same properties. So when you map the model, which is an AuthorRequest, you properly get a filled Author object back.
However, when you try to map the same AuthorRequest to a (single) Book then that won’t work since they are just too different: AuthorRequest has the properties Name, Surname, and Books, while Book has the properties Title and Author. There is simply no match there.
What you can do is map the books that are within the AuthorRequest object, doing this:
var bookEntities = model.Books.Adapt<Book[]>();
This will not map the collection of BookRequest objects that are within the model to a Book array. This should work as expected.
Note though that Mapster is already smart enough to map nested types on its own. If you look at the authorEntity object, you will see that the nested book entities were also properly mapped. So if you want to map the AuthorRequest (which contains BookRequests) to an Author entity containing Book entities, then your first line should already be enough:
var r = new AuthorRequest
{
Name = "William",
Surname = "Shakespeare",
Books = new BookRequest[] {
new BookRequest { Title = "Macbeth" },
new BookRequest { Title = "Romeo and Juliet" },
},
};
r.Adapt<Author>().Dump();

Related

Neo4jClient Create is including a JsonIgnore property

I have a C# class with simple properties I am expecting to be saved to a Node when I call create. When I build the query using Neo4jClient 5.1.3, a complex class that is decorated with [JsonIgnore] is included in the CREATE command.
Here is a Person class:
public class Person {
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
[JsonIgnore]
public Organization Employer { get; set; }
}
I'm creating an instance of it:
var person = new Person
{
Email = "absdfasdf#12341234.com",
FirstName = "Job",
LastName = "Bob",
Employer = previouslyCreatedEmployer,
};
...and when I run this:
var query = neo.Client.Cypher
.Create("(p:Person:Security $object)")
.WithParams(new Dictionary<string, object> {
{ "object", person }
})
;
the QueryTextForDebug is this:
CREATE (p:Person:Security {
Email: "absdfasdf#12341234.com",
FirstName: "Job",
LastName: "Bob",
Employer: {
Logo: "",
IxId: "89becda5-98b4-4623-928f-4c6580cd554f",
name: "Joe's Bakery"
},
IxId: "eb117b37-b062-40e1-b038-1749156ecf7a"
})
Since I had the JsonIgnore tag on Employer in class definition I was not expecting it to be included. What am I missing? And thank you.
Side thought... maybe I SHOULDN'T have the JsonIgnore tag in there so I can write nicer queries, right?
Neo4jClient v5.1.3 depends on Newtonsoft.Json, check that JsonIgnore comes from this library and not from System.Text.Json, or specify full type name including namespace:
public class Person
{
[Newtonsoft.Json.JsonIgnore]
public Organization Employer { get; set; }
}

Querying data from multiple tables using Dapper

I have the database table GTL_TITLES which has two foreign keys, AuthorId and PublisherId. If I want to query a title from the database, I want to also get the information from the AUTHOR and PUBLISHER tables. For this purpose, I created a stored procedure that joins all three tables and selects the following columns:
My GtlTitle Model class looks like this:
public string ISBN { get; set; }
public string VolumeName { get; set; }
public string TitleDescription { get; set; }
public string PublisherName { get; set; }
public DateTime PublicationDate { get; set; }
public Author TitleAuthor { get; set; }
public Publisher Publisher { get; }
As you could have guessed, class Author has two strings: FirstName and LastName and Publisher has PublisherName.
These being said, this is the method calling the database:
public GtlTitle GetTitle(string ISBN)
{
using (var connection = new SqlConnection(_connection))
{
connection.Open();
return connection.QuerySingle<GtlTitle>("GetTitleByISBN", new { ISBN }, commandType: CommandType.StoredProcedure);
}
}
And returns the following: {"isbn":"978-0-10074-5","volumeName":"Volume Name - 97581","titleDescription":"Description - 97581","publisherName":"Publisher - 714","publicationDate":"2020-05-23T00:00:00","titleAuthor":null,"publisher":null}
As you can see, titleAuthor and publisher are null. How can I fix this? Will I need to write fields like public string FirstName in the GtlTitle model class instead or is there any way of populating the Author and Publisher
as well?
Dapper supports multimapping with the splitOn parameter where you can split a row into mulitple objects by providing the column names where a new object begins.
return connection.Query<GtlTitle, Author, Publisher, GtlTitle>(sql,
(g,a,p) => {
g.TitleAuthor = a;
g.Publisher = p;
return g; },
splitOn: "FirstName,PublisherName").First();

How to add items to a property of type List when instantiating an object with object initializer?

I want the Books not to be null so I made it an initialized readonly property as follows.
class Author
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> Books { get; } = new List<string>();
}
Because of this constraint, I have to instantiate an object a first with object initializer and then populate its Books property via AddRange as follows.
class Project
{
void Main()
{
Author a = new Author
{
Id = 100,
Name = "Bill Gates"
};
a.Books.AddRange(new string[] { "Asp.net", "C++", "C#" });
}
}
Question
Is there any trick to make the instantiation of a (as well as populating its Books) simpler just with object initializer?
you can use private set
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> Books { get; private set; }
public Author(params string[] books)
{
Books = new List<string>(books);
}
}
or use just get (readonly option)
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> Books { get; }
public Author(params string[] books)
{
Books = new List<string>(books);
}
}
The private option I can change the instance of the list any time inside Author, but any other class outside Author just read it.
The read-only option I can't change any time. I initialize it in the constructor after that it will never change.
You can also use readonly collection, no body will add or remove books after the initialization.
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<string> Books { get; }
public Author(params string[] books)
{
Books = new ReadOnlyCollection<string>(books);
}
}
class Author
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> Books { get; } = new List<string>();
public Author(params string[] books)
{
Books.AddRange(books);
}
}
class Project
{
void Main()
{
Author a = new Author("Asp.net", "C++", "C#")
{
Id = 100,
Name = "Bill Gates"
};
}
}
Because of this constraint, I have to instantiate an object a first [emphasis mine]with object initializer and then populate its Books property via AddRange
But the client can set Books to null which is what I don't want.
These alarm bells are all answered by using a constructor. The constructor's purpose is creating valid objects. The constructor guarantees a valid object because it forces the user to provide the arguments required and then applies its internal state rules. Without a constructor the onus is on the user, and every user every time, to enforce that class' rules - and do it correctly.
It should strike the reader as ironic and incongruous that an object was instantiated in order to ensure partial state and then allowing external code to screw it up at will.
Edit
Re: the "am forced to" comment
You're focused on fixing only half the problem. For example, is it really ok for Name to be null or empty string? What's an Author without a name? Calling a method on a null Name string throws an exception. Either deal with everything it in the constructor or you'll be writing error trapping all over the place, including the client code. Client code should not be fixing Authors shortcomings.
But, if you must you can do this. And this "lazy initialization" does not eliminate the need for a decent constructor.
public List<string> Books {
get{ return value; }
set { if (Books == null) {
Books = new List<string>();
} else { Books = value; }
}
}
But that's buggy. An existing list can be wiped out.
public Author (string name, List<string> books = null) {
if(String.isNullOrWhiteSpace(name))
throw new ArgumentException("name is missing");
// P.S. no public setter properties to undo construction
Name = name;
Books = books;
}
// suggestion: rewrite to use params keyword to add 1 - many at once
public void AddBook( title) {
if (String.isNullORWhitespace(title)) return;
Books.Add(title); // allows duplicates, fyi.
If anyone feels the urge to up vote, up-vote #HugoJose answer instead. He effectively illustrated this earlier. I'm just trying to explain the goodness of a robust constructor.
end Edit

Is it normal to have a list of objects in an object?

I'm new to OO design, and I wondering whether it is typical to have designs where objects contain lists of other objects. An example is below:
// Person object containing a list of phone numbers
public class Person
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Phone> Contacts { get; set; }
public void AddPhoneNumber(Phone phone)
{
Contacts.Add(phoneNumber);
}
}
// Phone object
public class Phone
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Number { get; set; }
}
// Example code to establish an object:
Person p1 = new Person();
p1.FirstName = "John";
p1.LastName = "Smith";
p1.AddPhoneNumber(new Phone() { Number = "555957000" });
p1.AddPhoneNumber(new Phone() { Number = "555579561" });
Is there a better way to structure this that simplifies the design and allows easier access to the data? Thanks!
Yes, it is absolutely fine to have an object contains list of object. In OOPs this is called Composition, which represent strong relationship between participating class.
I wondering whether it is typical to have designs where objects
contain lists of other objects.
Absolutely, this is normal as an object can contain lists that belong to only that particular object. One of many examples is when you're traversing a binary tree or such you could have each node have their own list which identifies their children. There are many more cases in which an object should/could contain their own list.
Going back to your code, you seem to have an error because the code below states that the list will contain Phone objects.
public List<Phone> Contacts { get; set; }
but yet you're passing in a string object rather than a phone object.
public void AddPhoneNumber(string phoneNumber)
{
Contacts.Add(phoneNumber); // this code shouldnt compile
}
rather what you can do is this:
public void AddPhoneNumber(string phoneNumber)
{
Contacts.Add(new Phone() { Number = phoneNumber });
}
There's nothing wrong with your code. It's perfectly valid to have a list of objects within an object.
Though adding a string to your List of Phone objects will throw an error.
You can add a phone number like so:
Person p1 = new Person();
p1.Contacts.Add(new Phone() { Number = "555579561" });

Mapster - How to do ignore mapping for null properties

I'm using Mapster to map Dto instances to Model objects.
The Dtos are sent by a Javascript client, sending only the properties updated.
I would like to ignore null values, and have Mapster leave the model instance unchanged for this properties.
A simplified example to better explain the scenario:
// My .Net Dto class, used for client/server communication.
public class PersonDto
{
public string Id { get; set; }
public string Name { get; set; }
public string Family { get; set; }
}
// My Model class. Let's assume is the same data as per the Dto.
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public string Family { get; set; }
}
public void Update()
{
var existingPerson = new Person
{
Id = "A",
Name = "Ned",
Family = "Stark"
};
var patchDataSentFromClient = new PersonDto
{
Id = "A",
Name = "Rob"
};
patchDataSentFromClient.Adapt(existingPerson);
// Here existingPerson.Family should be "Stark", but it gets emptied out.
// the mapping should be equivalent to:
// if (patchDataSentFromClient.Family != null) existingPerson.Family = patchDataSentFromClient.Family;
}
Edit: the point is I don't want to write down the mapping condition for each of the thousands of properties in my Dtos. I want Mapster to Automap all string properties by name, but keep the "patch-like" logic of ignoring null values.
You can use IgnoreNullValues.

Categories