Automapper and Deep Loading - automapper

I have a question with regards to automapping entity framework objects which have been "deep loaded". I have a Project object which has a member called Tasks which is a collection. And each task has a member called Works which is a collection. When the Project is deep loaded, those Tasks and their Works member are all populated. It looks like this:
-- Project
-- Task 1
-- Work 1
-- Work 2
-- Task 2
-- Work 3
I have figured out how to map 1 level deep using a resolver like this:
CreateMap<DataAccess.Task, Entities.Task>()
.ForMember(obj => obj.Description, obj => obj.MapFrom(src => src.Description))
.ForMember(obj => obj.Estimate, obj => obj.MapFrom(src => src.Estimate))
.ForMember(obj => obj.Id, obj => obj.MapFrom(src => src.ID))
.ForMember(obj => obj.ProjectId, obj => obj.MapFrom(src => src.Project))
.ForMember(obj => obj.TaskName, obj => obj.MapFrom(src => src.TaskName))
.ForMember(obj => obj.Visible, obj => obj.MapFrom(src => src.Visible))
.ForMember(obj => obj.WorkItems, obj => obj.ResolveUsing<WorkItemsResolver>().FromMember(src => src.Works));
public class WorkItemsResolver : ValueResolver<EntityCollection<DataAccess.Work>, ICollection<Entities.Work>>
{
#region Overrides of ValueResolver<List<Task>,List<Task>>
protected override ICollection<Entities.Work> ResolveCore(EntityCollection<DataAccess.Work> source)
{
Mapper.EntitiesMapper entitiesMapper = new EntitiesMapper();
return source.Select(wk => entitiesMapper.Map<Entities.Work>(wk)).ToList();
}
#endregion
}
However, I cannot figure out how to go 2 levels deep i.e. to be able to map a top level object (Project) and have it's child lists (and their child lists) also get mapped to their concommitant business objects.
Does anyone know if this is possible with Automapper?
Thanks

You should have a corresponding DataAccess object for each Entity object. You already have a Task mapping, now you need one for Work. Also, don't use separate ValueResolvers. You can do anything you need using the lambda overloads in ForMember.
CreateMap<DataAccess.Task, Entities.Task>()
// when names match on both sides, there is no need to call ForMember
//.ForMember(obj => obj.Description, obj => obj.MapFrom(src => src.Description))
//.ForMember(obj => obj.Estimate, obj => obj.MapFrom(src => src.Estimate))
//.ForMember(obj => obj.Id, obj => obj.MapFrom(src => src.ID))
.ForMember(obj => obj.ProjectId, obj => obj.MapFrom(src => src.Project))
//.ForMember(obj => obj.TaskName, obj => obj.MapFrom(src => src.TaskName))
//.ForMember(obj => obj.Visible, obj => obj.MapFrom(src => src.Visible))
.ForMember(obj => obj.WorkItems, obj => obj.ResolveUsing(src =>
Mapper.Map<IEnumerable<DataAccess.Work>>(src.Works)))
;
CreateMap<DataAccess.Work, Entities.Work>()
// only call ForMember on properties that don't map automatically
;
Since you created a map for your 2'nd level of depth in the object hierarchy, you can just call Mapper.Map in the ResolveUsing lambda overload.

Related

AutoMapper ProjectTo not loading navigation properties for conditional mapping

I have the following mapping for a property
.ForMember(x => x.Destination, opt => opt.MapFrom(x =>
x.Request.PurposeId == (int)PurposeTypes.PickUpShipment
? x.Request.RequestDestinations.FirstOrDefault().Destination
: x.Request.ShippingDestinationDetails.FirstOrDefault(dd => dd.ShippingAddressTypeId == (int)ShippingRequestAddressType.Destination).ShippingAddress.Destination
))
And the destination object map is like this
CreateMap<Destination, DestinationDTO>()
.ForMember(x => x.LocationCode, opt => opt.MapFrom(x => x.LocationCode))
.ForMember(x => x.CountryCode, opt => opt.MapFrom(x => x.CountryCode))
.ForMember(x => x.CountryName, opt => opt.MapFrom(x => x.State.StateShortName)); // this is not working
The problem is when I am using ProjectTo the CountryName is not loading because it is not loading the navigation property State of the destination, so x.State.StateShortName is coming empty.
And I found out the reason behind it, this is happening because I am loading the destination conditionally and because of this it is not loading the navigation property, because if I remove the condition and loads the destination like this it works
.ForMember(x => x.Destination, opt => opt.MapFrom(x =>
x.Request.RequestDestinations.FirstOrDefault().Destination
))
So basically ProjectTo is not loading the navigation property if the property mapping is conditional and not direct.
Is there any way I can achieve this?

Bitrix24 webhook/REST API

I'd like to create TASK in bitrix24 with webhook URL. Almost everything is fine (task name, description, responsible, deadline etc...) the only problem is with field with several users (Observers and Participants). I don't know how to add these parameters to the URL :( The documentation is incomplete.
The solution is easy, I didn't see your code but per the API doc, you need to encode the JSON array of fields as parameters into the GET request: this will help you: How should I put json in GET request?
A better approach would be using POST to submit data instead of GET, this way you don't need to do the above, you can send the array as it.
This is the API doc you need to follow:
https://training.bitrix24.com/rest_help/tasks/task/tasks/tasks_task_add.php
Here is the array of fields you can set as the "fields" parameter when creating a task:
https://training.bitrix24.com/rest_help/tasks/fields.php
Example of an array of fields:
Array
(
[TITLE] => Process lead: +19196299012 - Outgoing call (set from business process)
[STAGE_ID] => 0
[DESCRIPTION] => Process lead: +19196299012 - Outgoing call
[DEADLINE] =>
[START_DATE_PLAN] =>
[END_DATE_PLAN] =>
[PRIORITY] => 0
[ACCOMPLICES] => Array
(
)
[AUDITORS] => Array
(
)
[TAGS] => Array
(
)
[ALLOW_CHANGE_DEADLINE] => Y
[ALLOW_CHANGE_DEADLINE_COUNT] =>
[ALLOW_CHANGE_DEADLINE_COUNT_VALUE] =>
[ALLOW_CHANGE_DEADLINE_MAXTIME] =>
[ALLOW_CHANGE_DEADLINE_MAXTIME_VALUE] =>
[TASK_CONTROL] => Y
[PARENT_ID] =>
[DEPENDS_ON] => Array
(
)
[GROUP_ID] => 0
[RESPONSIBLE_ID] => 1
[TIME_ESTIMATE] => 0
[ID] => 130
[CREATED_BY] => 1
[DESCRIPTION_IN_BBCODE] => Y
[DECLINE_REASON] =>
[REAL_STATUS] => 2
[STATUS] => 2
[RESPONSIBLE_NAME] => Mohamed Ali
[RESPONSIBLE_LAST_NAME] => Ouled Ameur
[RESPONSIBLE_SECOND_NAME] =>
[DATE_START] =>
[DURATION_FACT] =>
[DURATION_PLAN] =>
[DURATION_TYPE] => days
[CREATED_BY_NAME] => Mohamed Ali
[CREATED_BY_LAST_NAME] => Ouled Ameur
[CREATED_BY_SECOND_NAME] =>
[CREATED_DATE] => 2021-06-26T08:12:39+03:00
[CHANGED_BY] => 1
[CHANGED_DATE] => 2021-06-26T08:12:39+03:00
[STATUS_CHANGED_BY] => 1
[STATUS_CHANGED_DATE] => 2021-06-26T08:12:39+03:00
[CLOSED_BY] =>
[CLOSED_DATE] =>
[ACTIVITY_DATE] => 2021-06-26T08:12:39+03:00
[GUID] => {bcd84631-6c08-4124-8db2-7cfe90628dc1}
[MARK] =>
[VIEWED_DATE] => 2021-06-28T09:09:17+03:00
[TIME_SPENT_IN_LOGS] =>
[FAVORITE] => N
[ALLOW_TIME_TRACKING] => N
[MATCH_WORK_TIME] => N
[ADD_IN_REPORT] => Y
[FORUM_ID] =>
[FORUM_TOPIC_ID] =>
[COMMENTS_COUNT] =>
[SITE_ID] => s1
[SUBORDINATE] => Y
[FORKED_BY_TEMPLATE_ID] =>
[MULTITASK] => N
[UF_CRM_TASK] => Array
(
[0] => L_38
[1] => L_38
)
[UF_MAIL_MESSAGE] =>
[UF_TASK_WEBDAV_FILES] =>
)
to set more then one, you can use this. In the array every usernumber
$accomplice = ['1','2'];
$createTask = CRest::call('tasks.task.add',
...
'ACCOMPLICES' => $accomplice,
...

Automapper through relations in Entity Framework

As a newbie to automapper (v10.0.0) I'm trying to replace one of my queries. I currently use this to generate my response:
var query = from a in _context.ApprovalStatuses.AsNoTracking()
.Include(x => x.ApprovalOrder).ThenInclude(x => x.Worker)
where a.RequestId == request.Id
orderby a.ApprovalOrder.Position
let w = a.ApprovalOrder.Worker
select new RequestApprovalStatusDTO {
AssignedUtc = a.AssignedUtc,
Comments = a.Comments,
DecisionDateUtc = a.ApprovedDateUtc ?? a.RejectedDateUtc,
Email = w.Email,
Name = w.Name,
Uuid = a.Uuid
};
So I started by creating my mapping in my Profile subclass:
CreateMap<ApprovalStatus, RequestApprovalStatusDTO>()
.ForMember(x => x.DecisionDateUtc, x => x.MapFrom(y => y.ApprovedDateUtc ?? y.RejectedDateUtc))
.ForMember(x => x.Email, x => x.MapFrom(y => y.ApprovalOrder.Worker.Email))
.ForMember(x => x.Name, x => x.MapFrom(y => y.ApprovalOrder.Worker.Name));
And then I rewrote the query like so:
var query = _context.ApprovalStatuses
.Include(x => x.ApprovalOrder)
.ThenInclude(x => x.Worker)
.Where(x => x.RequestId == request.Id)
.OrderBy(x => x.ApprovalOrder.Position);
return Ok(_mapper.Map<RequestApprovalStatusDTO>(query));
At runtime, it's telling me
AutoMapperMappingException: Missing type map configuration or unsupported mapping.
Mapping types:
Object -> RequestApprovalStatusDTO
System.Object -> BoltOn.RequestApprovalStatusDTO
lambda_method(Closure , object , RequestApprovalStatusDTO , ResolutionContext )
I understand it's telling me that it doesn't know how to convert from object, but I'm not sure why it's trying to do that since query is an IOrderedQueryable<ApprovalStatus>.
Thanks to Lucian's pointer I was able to solve it like so:
var query = _context.ApprovalStatuses
.Where(x => x.Request.Uuid == uuid)
.OrderBy(x => x.ApprovalOrder.Position);
var approvals = await _mapper.ProjectTo<RequestApprovalStatusDTO>(query).ToArrayAsync();
if (approvals.Length == 0)
return NotFound();
return Ok(approvals);

How to apply extension hook in expressionengine

I'm working on expressionengine v2 and i have followed the docs at their site to make extension hooks, i've created a file in third party folder and install that from cp(control panel). Now i don't know how to call that function using hook just to show some text.
$hooks = array(
'after_channel_entry_insert' => 'after_channel_entry_insert',
'before_channel_entry_update' => 'before_channel_entry_update'
);
foreach ($hooks as $hook_name => $method_name) {
$data[] = array(
'class' => __CLASS__,
'method' => $method_name,
'hook' => $hook_name,
'settings' => serialize($this->settings),
'priority' => 10,
'version' => $this->version,
'enabled' => 'y'
);
}
//insert data in extension table
ee()->db->insert_batch('extensions', $data);
//now i want to call this function on hook
function after_channel_entry_insert($data = '') {
die('after_channel_entry_insert');
}

Automapper - Mapping Multiple Properties using 1 Resolver

Mapper.CreateMap<WorkItemSummary, WorkItemSummaryDto> ()
.ForMember(dto => dto.ProductDisplayName, opt => opt.MapFrom(src => src.Product.DisplayName))
.ForMember(dto => dto.TeamId, opt => opt.MapFrom(src => src.Product.Team.TeamId))
.ForMember(dto => dto.TeamName, opt => opt.MapFrom(src => src.Product.Team.TeamName))
.ForMember(dto => dto.RRConsolidated, opt => opt.ResolveUsing<WIConsolidateResolveReasonResolver>())
.ForMember(dto => dto.NewBugsQuery, opt => opt.ResolveUsing<WIQueryNewBugsResolver>())
.ForMember(dto => dto.ResolvedBugsQuery, opt => opt.ResolveUsing<WIQueryResolvedResolver>())
.ForMember(dto => dto.ClosedBugsQuery, opt => opt.ResolveUsing<WIQueryClosedResolver>())
.ForMember(dto => dto.BacklogQuery, opt => opt.ResolveUsing<WIQueryBacklogResolver>())
.ForMember(dto => dto.ResolvedReasonFixQuery, opt => opt.ResolveUsing<WIQueryResolvedReasonFixResolver>());
--- Adding One Resolver Code and mostly all resolvers are doing same Job but pulling different content from Xml Nodes
public class WIQueryNewBugsResolver: ValueResolver<WorkItemSummary, string>
{
protected override string ResolveCore(WorkItemSummary source)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(System.IO.Path.Combine(AppDomain.CurrentDomain.RelativeSearchPath, "WIsQuery.xml"));
....
}
}
I have multiple resolvers doing almost same job of reading some xmls and getting the data. Thinking of consolidating all those calls under one resolver.
ex: WIConsolidateResolveReasonResolver, WIQueryResolvedReasonFixResolver, WIQueryBacklogResolver
In this regard, can I call one resolver once and get all the other properties populated in one call.
Is there any other method call in place of ForMember
Thanks
use "forallmembers" method of automapper if same job you are doing.may be it will help for you.

Resources