How to map array to array using Automapper in C# - c#

I have below Source and Target class.
public class Source
{
public string BookName { get; set; }
public Bookstore[] BookStore { get; set; }
}
public class Bookstore
{
public Address[] Address { get; set; }
}
public class Address
{
public string Street { get; set; }
}
Target class as below
public class Target
{
public string Book { get; set; }
public Store[] Store { get; set; }
}
public class Store
{
public Geo Geo { get; set; }
}
public class Geo
{
public Location[] Location { get; set; }
}
public class Location
{
public string Street { get; set; }
}
I am looking for solution on proper mapping where source file data gets copied to target fields like below,
CreateMap<Source, Target>()
.ForMember(dest => dest.Book, o => o.MapFrom(src => src.BookName));
CreateMap<Bookstore, Store>()
.ForMember(dest => dest.Geo.Location, o => o.MapFrom(src => src.Address));
But I see a few errors like "resolve to top-level member" etc or Array fields, Geo and Location remains empty.
I would like to map source data to the destination.
Below is a source file example
var jsonText = #"
{
"BookName": "Test",
"BookStore": [
{
"Address": [
{
"Street": "1234"
}
]
}
]
}";
var sourType = JsonConvert.DeserializeObject<Source>(jsonText);
The target file expected is as below, where "Geo" object is added,
{
"Book": "Test",
"Store": [
{
"Geo": {
"Location": [
{
"Street": "1234"
}
]
}
}
]
}

You need a custom converter for this type of wrapping. Though your JSON result should be like this according to your model:
{
"Book": "Test",
"Store": [
{
"Geo": {
"Location": [
{
"Street": "1234"
}
]
}
}
]
}
Btw here is the solution:
Expression < Func < Bookstore, Store >> storeConverter = p => (new Store {
Geo = new() {
Location = p.Address
.Select(ad => new Location {
Street = ad.Street
}).ToArray()
}
});
CreateMap < Source, Target > ()
.ForMember(dest => dest.Book, o => o.MapFrom(src => src.BookName))
.ForMember(dest => dest.Store, o => o.MapFrom(src => src.BookStore));
CreateMap < Bookstore, Store > ()
.ConvertUsing(storeConverter);
You can move the expression in a separate class type of converter for more cleaner code. see details in the documentation https://docs.automapper.org/en/stable/Custom-type-converters.html

Related

How to write this MongoDB (Aggregate) query into C#

I need to perform group-by with the $max operator in MongoDB. I figure out the query which is working in the MongoDB database but was not able to write the same using C#.
db.getCollection('Employee').aggregate(
[
{$unwind : "$Projects"},
{
"$group" : {
"_id" : "$EmpId",
"LastUpdated" : {"$max" : "$Projects.LastUpdated"}
}
}
]);
Below C# code is giving an error:
"Projects" is not valid property.
_db.GetCollection<BsonDocument>(collection).Aggregate().Unwind(i=>i.Projects)
Assume this is your sample data:
[{
"EmpId": 1,
"Projects": [
{
"LastUpdated": {
"$date": "2021-10-22T16:00:00Z"
}
},
{
"LastUpdated": {
"$date": "2021-11-07T16:00:00Z"
}
},
{
"LastUpdated": {
"$date": "2022-01-22T16:00:00Z"
}
}
]
}]
and this is your model class:
public class Employee
{
public int EmpId { get; set; }
public List<EmployeeProject> Projects { get; set; }
}
public class EmployeeProject
{
public DateTime UpdatedDate { get; set; }
}
To use the Projects property, you need to specify your collection as Project type as:
_db.GetCollection<Employee>("Employee")
Solution 1: Mix use of AggregateFluent and BsonDocument
var result = _db.GetCollection<Employee>("Employee")
.Aggregate()
.Unwind(i => i.Projects)
.Group(new BsonDocument
{
{ "_id", "$EmpId" },
{ "LastUpdated", new BsonDocument("$max", "$Projects.LastUpdated") }
})
.ToList();
Solution 2: Full use of AggregateFluent
Pre-requisites:
Need to create a model for unwinded Project.
public class UnwindEmployeeProject
{
public int EmpId { get; set; }
public EmployeeProject Projects { get; set; }
}
var result = _db.GetCollection<Employee>("Employee")
.Aggregate()
.Unwind<Employee, UnwindEmployeeProject>(i => i.Projects)
.Group(
k => k.EmpId,
g => new
{
EmpId = g.Key,
LastUpdated = g.Max(x => x.Projects.LastUpdated)
})
.ToList();
Solution 3: Full use of BsonDocument
With Mongo Compass, you can export your query to C#.
PipelineDefinition<Employee, BsonDocument> pipeline = new BsonDocument[]
{
new BsonDocument("$unwind", "$Projects"),
new BsonDocument("$group",
new BsonDocument
{
{ "_id", "$EmpId" },
{ "LastUpdated",
new BsonDocument("$max", "$Projects.LastUpdated") }
})
};
var result = _db.GetCollection<Employee>("Employee")
.Aggregate(pipeline)
.ToList();
Output

How to groupby list in c# and show result without foreach?

I am using .net core 5 web api project. I have following classes:
public class GroupedShowbackSummaryListDto
{
public int RuleId { get; set; }
public string RuleName { get; set; }
public decimal TotalPrice { get; set; }
public List<GroupedProjectForShowbackSummary> Projects { get; set; }
}
public class GroupedProjectForShowbackSummary
{
public int? Id { get; set; }
public string Name { get; set; }
public decimal TotalPrice { get; set; }
}
I would like group by rule id and show list of projects as response, I am trying:
var queryable = _context.ShowbackSummaries.Where(x => x.ProjectId != null).AsQueryable();
var summary = queryable.ToList();
var grouped = summary.GroupBy(x => x.ShowbackRuleId).Select(c => new GroupedShowbackSummaryListDto
{
RuleId = c.Key,
RuleName = c.Select(f => f.ShowbackRule.Name).First(),
TotalPrice = c.Select(f => f.Price).Sum(),
Projects = new List<GroupedProjectForShowbackSummary>
{
new()
{
Id = c.Select(f => f.ProjectId).First(),
Name = c.Select(f => f.Project.Name).First(),
TotalPrice = c.Select(f => f.Price).First()
}
}
}).ToList();
return grouped;
I know I am using first and it returns only first project but I would like to return all, if I switch to list int id for ex, it will show me:
[
100,
101,
...
]
I would like multiple result for my current response:
[
{
"ruleId": 1,
"ruleName": "rule-1",
"totalPrice": 400,
"projects": [
{
"id": 1169,
"name": "lubos-cncf",
"totalPrice": 200
}
]
},
{
"ruleId": 2,
"ruleName": "rule-2",
"totalPrice": 300,
"projects": [
{
"id": 1169,
"name": "lubos-cncf",
"totalPrice": 300
}
]
}
]
P.S. Get all projects like this.
This should work (inside your GroupBy-Expression):
Projects = c.Select(f =>
new GroupedProjectForShowbackSummary()
{
Id = f.ProjectId,
Name = f.Project.Name,
TotalPrice = f.Price
}).ToList()

AutoMapper mapping not working properly JObject/Json

I'm trying to map JObject to POCO class using Automapper, It mapped only same name property and ignore method does not work.
I'm not sure AutoMapper support Json to POCO class mapping or Is it an issue with my configuration?
internal class Program
{
private static void Main(string[] args)
{
AutoMapperConfiguration.Configure();
string json = File.ReadAllText("jsonData.json");
JArray assets = JArray.Parse(json);
List<AssetDTO> assetDTOList = Mapper.Map<List<AssetDTO>>(assets);
}
}
public class AutoMapperConfiguration
{
public static void Configure()
{
AutoMapper.Mapper.Initialize(x => x.AddProfile<AssetProfile>());
}
}
public class AssetProfile : Profile
{
public AssetProfile()
{
CreateMap<JObject, AssetDTO>()
.ForAllMembers(dest => dest.Ignore());
CreateMap<JObject, AssetDTO>()
.ForMember(dest => dest.AssetId, o => o.MapFrom(j => j["AssetId"]))
.ForMember(dest => dest.Type, o => o.MapFrom(j => j["AssetType"]))
.ForMember(dest => dest.DeviceSerialNumber, o => o.MapFrom(j => JArray.Parse(j["Device"].ToString())[0]["SerialNumber"]));
}
}
public class AssetDTO
{
public string AssetId { get; set; }
public string DeviceSerialNumber { get; set; }
public string Type { get; set; }
}
jsonData.json
[
{
"AssetType": "Paver",
"AssetId": "PaverId100",
"ContactName": "ContactName",
"Description": "description",
"Device": [
{
"Make": "BP02",
"SerialNumber": "BPPA0001",
"Ucid": null,
"Address": {
"AddressLine1": "address 1",
"AddressLine2": "add 2",
"City": "city",
"State": "B'lore",
"ZipCode": "124578",
"Country": "India"
}
}
]
}
]
Github repo for sample code
For now I resolved this. The issue I observed that Jarray does not work like List and Array, so I iterated over JArray and convert each JObject to POCO.
private static void Main(string[] args)
{
AutoMapperConfiguration.Configure();
string json = File.ReadAllText("jsonData.json");
JArray assets = JArray.Parse(json);
List<AssetDTO> assetDTOList = new List<AssetDTO>();
for (int i = 0; i < assets.Count; i++)
{
JObject jObject = JObject.Parse(assets[i].ToString());
AssetDTO assetDTO = Mapper.Map<AssetDTO>(jObject);
assetDTOList.Add(assetDTO);
}
}
Github repo for sample code

EF Core Many to Many using Automapper returns loop on API Resource

I'm using .net Core with EF2 and I want to setup this Many to Many relationship. Any Doctor can have many Appointments and vice versa. Also I want to implement Automapper mapping on top.
High Level Model Structure
**Doctor**
- Id
- Name
- Appointments
**Appointment**
- Id
- Name
- Category
- Doctors
Doctors Model
[Table("Doctors")]
public class Doctor
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<DoctorAppointment> Appointments { get; set; }
public Doctor()
{
Appointments = new Collection<DoctorAppointment>();
}
}
Appointments Model
[Table("Appointments")]
public class Appointment
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public ICollection<DoctorAppointment> Doctors { get; set; }
public Appointment()
{
Doctors = new Collection<DoctorAppointment>();
}
}
Many to Many Doctors-Appointments Linking Table
[Table("DoctorAppointments")]
public class DoctorAppointment
{
public int DoctorId { get; set; }
public int AppointmentId { get; set; }
public Doctor Doctor { get; set; }
public Appointment Appointment { get; set; }
}
DB context
public class MyDocDbContext : DbContext
{
public DbSet<Appointment> Appointments { get; set; }
public DbSet<Doctor> Doctors { get; set; }
public MyDocDbContext(DbContextOptions<MyDocDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<DoctorAppointment>().HasKey(da =>
new { da.DoctorId, da.AppointmentId });
modelBuilder.Entity<DoctorAppointment>()
.HasOne(da => da.Doctor)
.WithMany(p => p.Appointments)
.HasForeignKey(pt => pt.DoctorId);
modelBuilder.Entity<DoctorAppointment>()
.HasOne(pt => pt.Appointment)
.WithMany(t => t.Doctors)
.HasForeignKey(pt => pt.AppointmentId);
}
}
Doctors and Appointment Controllers:
[HttpGet("appointment/{id}")]
public async Task<IActionResult> GetAppointment(int id)
{
var app = await context.Appointments
.Include(a => a.Doctors)
.ThenInclude(da => da.Doctor)
.SingleOrDefaultAsync(a => a.Id == id);
return Ok(app);
}
[HttpGet("doctor/{id}")]
public async Task<IActionResult> GetDoctor(int id)
{
var doc = await context.Doctors
.Include(d => d.Appointments)
.ThenInclude(da => da.Appointment)
.SingleOrDefaultAsync(v => v.Id == id);
return Ok(doc);
}
Now implementing Automapper Resource Mapping...
Until this point everything works fine, as when fetching on of those two entities it returns all the relations. The problem comes when using Automapper to map the Domain to API Resource. Using the following mapping resources:
DoctorResource
public class DoctorResource
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<AppointmentResource> Appointments { get; set; }
public DoctorResource()
{
Appointments = new Collection<AppointmentResource>();
}
}
AppointmentResource
public class AppointmentResource
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public ICollection<DoctorResource> Doctors { get; set; }
public AppointmentResource()
{
Doctors = new Collection<DoctorResource>();
}
}
Finally, the mapping is the following:
public class MappingProfile : Profile
{
public MappingProfile()
{
// Domain to API Resource
CreateMap<Doctor, DoctorResource>()
.ForMember(dr => dr.Appointments, opt => opt
.MapFrom(d => d.Appointments
.Select(y => y.Appointment)
.ToList()));
CreateMap<Appointment, AppointmentResource>()
.ForMember(dr => dr.Doctors, opt => opt
.MapFrom(d => d.Doctors
.Select(y => y.Doctor)
.ToList()));
}
}
So, when NOT using the mapping resources, the JSON result is this (the wanted one):
**http://localhost:5000/api/appointment/1**
{
"id": 1,
"name": "Personal Trainer",
"category": "Fitness",
"doctors": [
{
"doctorId": 2027,
"appointmentId": 1,
"doctor": {
"id": 2027,
"name": "Dr. Cunha",
"appointments": [],
}
},
{
"doctorId": 2028,
"appointmentId": 1,
"doctor": {
"id": 2028,
"name": "Dr. Gouveia",
"appointments": [],
}
},
{
"doctorId": 2029,
"appointmentId": 1,
"doctor": {
"id": 2029,
"name": "Dr. Tiago",
"appointments": [],
}
}
]
}
However when using Automapper to map the API response it expands the Appointments category and the the Doctors again, and so on...
**http://localhost:5000/api/appointment/1**
{
"id": 1,
"name": "Personal Trainer",
"category": "Fitness",
"doctors": [
{
"id": 2027,
"name": "Dr. Cunha",
"appointments": [
{
"id": 1,
"name": "Personal Trainer",
"category": "Fitness",
"doctors": [
{
"id": 2028,
"name": "Dr. Gouveia",
"appointments": [
{
"id": 1,
"name": "Personal Trainer",
"category": "Fitness",
"doctors": [
{ ....
and so on...
However for some unknown reason to me the Doctor Controller returns the desired output.
Sorry about the lengthy post, but I couldn't provide you with a clear picture without exposing the whole thing.
Cheers
I assume that you'r MappingProfile should be change like bellow:
public MappingProfile()
{
// Domain to API Resource
CreateMap<Doctor, DoctorResource>()
.ForMember(dr => dr.Appointments, opt => opt
.MapFrom(d => d.DoctorAppointment
.Select(y => y.Appointment)
.ToList()));
CreateMap<Appointment, AppointmentResource>()
.ForMember(dr => dr.Doctors, opt => opt
.MapFrom(d => d.DoctorAppointment
.Select(y => y.Doctor)
.ToList()));
}
Instead of MapFrom(d => d.Doctors) and also MapFrom(d => d.Appointments)
replace them with MapFrom(d => d.DoctorAppointment)
it should be work.

Elasticsearch, how to make NEST map response to class

First of all, I am using NEST 5.5.0.
I have the following use of a remote elasticsearch-index:
var node = new Uri("http://distribution.virk.dk/cvr-permanent");
var settings = new
ConnectionSettings(node).DefaultIndex("virksomhed");
settings.BasicAuthentication("username", "password");
var client = new ElasticClient(settings);
var searchResponse = client.Search<Company>(s => s
.AllTypes().Query(q => q
.Match(m => m
.Field(f => f.cvrNumber)
.Query("35954716")
)
)
);
The mappings in the index (without a bunch of other properties besides cvrNummer) are as follows:
{
"cvr-permanent-prod-20170205" : {
"mappings" : {
"virksomhed" : {
"_size" : {
"enabled" : true
},
"properties" : {
"Vrvirksomhed" : {
"properties" : {
"type" : "long"
},
"cvrNummer" : {
"type" : "string"
},
}
}
},
}
}
}
}
}
I also have the following class which the result is supposed to be mapped to:
[ElasticsearchType(Name = "virksomhed")]
public class Company
{
[Text(Name = "Vrvirksomhed.cvrNummer")]
public string cvrNumber { get; set; }
}
Now, the search (searchResponse) holds the expected results (1 result), where the part concerning cvrNummer looks as follows:
"hits": {
"total": 1,
"max_score": 17.34601,
"hits": [
{
"_index": "cvr-permanent-prod-20170205",
"_type": "virksomhed",
"_id": "4000333383",
"_score": 17.34601,
"_source": {
"Vrvirksomhed": {
"cvrNummer": 35954716,
"regNummer": [
{
"regnummer": "A/S35855",
"periode": {
"gyldigFra": "1956-06-01",
"gyldigTil": "1999-10-18"
},
"sidstOpdateret": "2015-02-10T00:00:00.000+01:00"
}
],
"brancheAnsvarskode": null,
"reklamebeskyttet": false,
"navne": [
...
However, when i look in searchResponse.Documents, I have the correct type (Company), but the value of cvrNumber is null.
Any ideas what I'm doing wrong, since the value of cvrNummer is not mapped into cvrNumber on the instance of Company in searchResponse.Documents?
Thanks in advance for your input!
UPDATE
I tried the following without success, still got the expected result, but cvrNumber is still null (in searchResponse.Documents):
[ElasticsearchType(Name = "virksomhed")]
public class Company
{
[Object(Name = "Vrvirksomhed")]
public Vrvirksomhed Vrvirksomhed { get; set; }
}
public class Vrvirksomhed
{
[Text(Name = "cvrNummer")]
public string cvrNumber { get; set; }
}
With the query:
var searchResponse = client.Search<Vrvirksomhed>(s => s
.AllTypes().Query(q => q
.Match(m => m
.Field(f => f.cvrNumber)
.Query("35954716")
)
)
);
UPDATE
It works with the following modifications to the query:
var searchResponse = client.Search<Company>(s => s
.AllTypes().Query(q => q
.Match(m => m
.Field(f => f.Vrvirksomhed.cvrNumber)
.Query("35954716")
)
)
);
[ElasticsearchType(Name = "virksomhed")]
public class Company
{
[Text(Name = "Vrvirksomhed.cvrNummer")]
public string cvrNumber { get; set; }
}
Vrvirksomhed looks like it should be a POCO property on Company mapped either as an object datatype or nested datatype (take a look at nested objects in the Definitive Guide for the differences), where that POCO has a property called cvrNumber, similar to
[ElasticsearchType(Name = "virksomhed")]
public class Company
{
[Object(Name = "Vrvirksomhed")]
public Vrvirksomhed Vrvirksomhed { get; set; }
}
public class Vrvirksomhed
{
[Text(Name = "cvrNummer")]
public string cvrNumber { get; set; }
}

Categories

Resources