How do I configure the mapper so that this works?
(i.e. the properties from the dynamic object should map to the properties of the class definition with the same letters - ignoring case)
public class Foo {
public int Bar { get; set; }
public int Baz { get; set; }
}
dynamic fooDyn = new MyDynamicObject();
fooDyn.baR = 5;
fooDyn.bAz = 6;
Mapper.Initialize(cfg => {});
Foo result = Mapper.Map<Foo>(fooDyn);
result.Bar.ShouldEqual(5);
result.Baz.ShouldEqual(6);
If your dynamic object implements IDictionary<string,object> (e.g. ExpandoObject) then the following should work. There must be some easier way to do this as anonymous objects are mapped just fine even if the case is different.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<IDictionary<string, object>, Foo>()
.ConstructUsing(d =>
{
Foo foo = new Foo();
foreach (System.Reflection.PropertyInfo prop in typeof(Foo).GetProperties())
{
foreach (KeyValuePair<string, object> entry in d)
{
if (entry.Key.ToLowerInvariant() == prop.Name.ToLowerInvariant())
{
prop.SetValue(foo, entry.Value);
break;
}
}
}
return foo;
});
});
AutoMapper allows you to configure explicit member mapping on the map configuration in this style:
var config = new MapperConfiguration(cfg =>
{
var dynamicMap = cfg.CreateMap<IDictionary<string, object>, SomethingDTO>();
dynamicMap.ForAllMembers((expression) => expression.MapFrom(source =>
source.ContainsKey(expression.DestinationMember.Name.Substring(0, 1).ToLower()
+ expression.DestinationMember.Name.Substring(1))
? source[expression.DestinationMember.Name.Substring(0, 1).ToLower()
+ expression.DestinationMember.Name.Substring(1)] : null
));
});
For mapping a dynamic/expando object that is camel case to an type with pascal case members you could use ForAllMembers on the the explicit map configuration. Possible use case: json payloads to DTO.
In comparison to the other answer (which also works) this approach allows you to continue to benefit and use all the other map features and configuration.
Related
I'm using Dapper Extensions and have defined my own custom mapper to deal with entities with composite keys.
public class MyClassMapper<T> : ClassMapper<T> where T : class
{
public MyClassMapper()
{
// Manage unmappable attributes
IList<PropertyInfo> toIgnore = typeof(T).GetProperties().Where(x => !x.CanWrite).ToList();
foreach (PropertyInfo propertyInfo in toIgnore.ToList())
{
Map(propertyInfo).Ignore();
}
// Manage keys
IList<PropertyInfo> propsWithId = typeof(T).GetProperties().Where(x => x.Name.EndsWith("Id") || x.Name.EndsWith("ID")).ToList();
PropertyInfo primaryKey = propsWithId.FirstOrDefault(x => string.Equals(x.Name, $"{nameof(T)}Id", StringComparison.CurrentCultureIgnoreCase));
if (primaryKey != null && primaryKey.PropertyType == typeof(int))
{
Map(primaryKey).Key(KeyType.Identity);
}
else if (propsWithId.Any())
{
foreach (PropertyInfo prop in propsWithId)
{
Map(prop).Key(KeyType.Assigned);
}
}
AutoMap();
}
}
I also have this test case to test my mapper:
[Test]
public void TestMyAutoMapper()
{
DapperExtensions.DapperExtensions.DefaultMapper = typeof(MyClassMapper<>);
MySubscribtionEntityWithCompositeKey entity = new MySubscribtionEntityWithCompositeKey
{
SubscriptionID = 145,
CustomerPackageID = 32
};
using (var connection = new SqlConnection(CONNECTION_STRING))
{
connection.Open();
var result = connection.Insert(entity);
var key1 = result.SubscriptionID;
var key2 = result.CustomerPackageID;
}
}
Note that I set the default mapper in the test case.
The insert fails and I notive that my customer mapper is never called. I have no documentation on the github page on the topic, so I'm not sure if there's anything else I need to do to make dapper extensions use my mapper.
Thanks in advance!
Looking at your question, you are attempting to write your own defalut class mapper derived from the existing one. I never used this approach; so I do not know why it is not working or whether it should work.
I explicitly map the classes as below:
public class Customer
{
public int CustomerID { get; set; }
public string Name { get; set; }
}
public sealed class CustomerMapper : ClassMapper<Customer>
{
public CustomerMapper()
{
Schema("dbo");
Table("Customer");
Map(x => x.CustomerID).Key(KeyType.Identity);
AutoMap();
}
}
The AutoMap() will map rest of the properties based on conventions. Please refer to these two resources for more information about mapping.
Then I call SetMappingAssemblies at the startup of the project as below:
DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { Assembly.GetExecutingAssembly() });
The GetExecutingAssembly() is used in above code because mapping classes (CustomerMapper and other) are in same assembly which is executing. If those classes are placed in other assembly, provide that assembly instead.
And that's it, it works.
To set the dialect, I call following line just below the SetMappingAssemblies:
DapperExtensions.DapperExtensions.SqlDialect = new DapperExtensions.Sql.SqlServerDialect();
Use your preferred dialect instead of SqlServerDialect.
Apparently, the solution mentioned here may help you achieve what you are actually trying to. But, I cannot be sure, as I said above, I never used it.
I have few classes and they have multiple list items like below:
public class Request1
{
public List<AdditionalApplicantData> AdditionalApplicantData { get; set;}
public List<ApplicantData> ApplicantData { get; set; }
}
public class Request2
{
public List<ApplicantDetails> ApplicantData { get; set; }
}
I want to map Request1 to Request2 but List of ApplicantData has to be mapped from multiple sources like List of ApplicantData & List of AdditionalApplicantData but not sure how to achieve it can someone please help me here?
You can use function below with createMap() function. Source: https://github.com/AutoMapper/AutoMapper/wiki/Before-and-after-map-actions
.AfterMap((src, dest) => {
dest.ApplicantData = /*your logic here*/
});
And you should mark ApplicantData as don't map because you have a variable named ApplicantData at the source class. You should implement the logic yourself.
EDIT:
When you are initializing mapper, you create map for each object. So for your case it would be like:
Mapper.Initialize(cfg => {
cfg.CreateMap<Request1, Request2>()
.ForMember(x => x.ApplicantData, opt => opt.Ignore()) //You want to implement your logic so ignore mapping
.AfterMap((src, dest) =>
{
dest.ApplicantData = /*implement your logic here*/
});
});
public class ApplicantDetailsResolver : IValueResolver<Request1, Request2, List<ApplicantDetails>>
{
public List<ApplicantDetails> Resolve(Request1 source, Request2 destination,List<ApplicantDetails> destMember, ResolutionContext context)
{
destination.ApplicantDetails = context.Mapper.Map<List<ApplicantDetails>>(source.ApplicantData);
for (int i = 0; i < destination.ApplicantDetails.Count(); i++)
{
context.Mapper.Map(source.AdditionalApplicantData.ElementAt(i), destination.ApplicantDetails.ElementAt(i));
}
return destination.ApplicantDetails;
}
}
I have written above custom value resolver for mapping list from multiple sources and its working fine but problem, is it can't match properties which are differently named, is there way I can handle this scenario as well?
I'm sure I am missing something simple. First, I'll show all the code I have written to wire up the plumbing, then I'll show the exception message. Then, I'll set out what I have tried to fix it.
LicenceTrackerProfile
public class LicenceTrackerProfile : Profile
{
const string LicenceTrackerProfileName = "LicenceTrackerProfile";
public override string ProfileName
{
get { return LicenceTrackerProfileName; }
}
protected override void Configure()
{
// initialize mappings here
new ViewModelMappings(this).Initialize();
}
}
MapperBootstrapper
public class MapperBootstrapper
{
public void Configure()
{
var profile = new LicenceTrackerProfile();
AutoMapper.Mapper.Initialize(p => p.AddProfile(profile));
}
}
MappingBase
public abstract class MappingBase
{
private readonly Profile _profile;
protected MappingBase(Profile profile)
{
_profile = profile;
_profile.SourceMemberNamingConvention = new PascalCaseNamingConvention();
_profile.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
}
public Profile Profile
{
get { return _profile; }
}
}
UniversalMapper
public class UniversalMapper : IUniversalMapper
{
private readonly IMappingEngine _mappingEngine;
public UniversalMapper(IMappingEngine mappingEngine)
{
_mappingEngine = mappingEngine;
}
public virtual TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
{
return _mappingEngine.Map(source, destination);
}
}
ViewModelMappings
public class ViewModelMappings : MappingBase, IMappingInitializer
{
private readonly Profile _profile;
public ViewModelMappings(Profile profile) : base(profile)
{
_profile = profile;
_profile.SourceMemberNamingConvention = new PascalCaseNamingConvention();
_profile.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
}
public void Initialize()
{
// data to domain mappings
Profile.CreateMap<EFDTO.Enums.FileTypes, Domain.FileTypes>();
Profile.CreateMap<EFDTO.Licence, Domain.Licence>();
Profile.CreateMap<EFDTO.LicenceAllocation, Domain.LicenceAllocation>();
Profile.CreateMap<EFDTO.Person, Domain.Person>();
Profile.CreateMap<EFDTO.Software, Domain.Software>();
Profile.CreateMap<EFDTO.SoftwareFile, Domain.SoftwareFile>();
Profile.CreateMap<EFDTO.SoftwareType, Domain.SoftwareType>();
}
}
Note, the initialize method and Configure method are being called, so they're not being "missed".
Exception
Missing type map configuration or unsupported mapping.
Mapping types: Software -> Software LicenceTracker.Entities.Software
-> LicenceTracker.DomainEntities.Software
Destination path: Software
Source value: LicenceTracker.Entities.Software
Troubleshooting
Ignoring columns. I planned to ignore columns, starting with all and then eliminating them by un-ignoring them 1 by 1 until I found the problem columns. However, to my surprise, the error occurs when I ignore all columns:
Profile.CreateMap<EFDTO.Software, Domain.Software>()
.ForMember(software => software.Licences, e => e.Ignore())
.ForMember(software => software.Name, e => e.Ignore())
.ForMember(software => software.SoftwareFiles, e => e.Ignore())
.ForMember(software => software.Type, e => e.Ignore())
.ForMember(software => software.Description, e => e.Ignore())
.ForMember(software => software.Id, e => e.Ignore())
.ForMember(software => software.TypeId, e => e.Ignore()
.ForMember(software => software.ObjectState, e => e.Ignore());
The Domain entities have [DataContract] (at class level) and [DataMember] (at method level) attributes. I added each of those attributes to the EF entities as well.
Other than that, I am out of ideas. It all seems to be wired up correctly.
What did I miss?
I'm back to heroically answer my question.
The problem was in the Service which created the UniversalMapper object (forgive the sloppy code, it is not final yet):
public class LicenceTrackerService : ILicenceTrackerService, IDisposable
{
LicenceTrackerContext context = new LicenceTrackerContext();
private MapperBootstrapper mapperBootstrapper;
private IUniversalMapper mapper = new UniversalMapper(Mapper.Engine);
private IUnitOfWork unitOfWork;
public LicenceTrackerService()
{
unitOfWork = new UnitOfWork(context, new RepositoryProvider(new RepositoryFactories()));
mapperBootstrapper = new MapperBootstrapper();
mapperBootstrapper.Configure();
Database.SetInitializer(new LicenceTrackerInitializer());
context.Database.Initialize(true);
}
public int GetNumber()
{
return 42;
}
public List<LicenceTracker.DomainEntities.Software> GetSoftwareProducts()
{
var productsRepo = unitOfWork.Repository<Software>();
var list = productsRepo.Query().Select().ToList();
var softwareList = new List<LicenceTracker.DomainEntities.Software>();
foreach (var software in list)
{
var softwareProduct = new LicenceTracker.DomainEntities.Software();
softwareList.Add(Mapper.Map(software, softwareProduct));
}
return softwareList;
}
public void Dispose()
{
unitOfWork.Dispose();
}
}
I'm still not sure why, but initializing the mapper outside of the constructor (default value style) was not happy. By moving that instantiation into the constructor of the service, it worked:
private IUniversalMapper mapper;
public LicenceTrackerService()
{
mapper = new UniversalMapper(Mapper.Engine);
...
}
There's obviously something about static properties (Mapper.Engine) and default instantiations that I'm not understanding.
Anyway, no big deal as I was planning to inject the UniversalMapper into the service anyway.
Edit
I've actually figured out the problem for real now. It is an ordering thing. With Automapper, I had to initialize the mapper with the Profile before inserting the Mapper.Engine into the UniversalMapper.
Obviously, the Get aspect of the Mapper.Engine property is not just a memory reference to an object. And yes, a quick glance at the code inside Automapper confirms that.
So, assigning the result of the Get property to the _mappingEngine field of the UniversalMapper must happen after that engine has been configured.
I am creating a rule set engine that looks kinda like a unit test framework.
[RuleSet(ContextA)]
public class RuleSet1
{
[Rule(TargetingA)]
public Conclusion Rule1(SubjectA subject)
{ Create conclusion }
[Rule(TargetingA)]
public Conclusion Rule2(SubjectA subject)
{ Create conclusion }
[Rule(TargetingB)]
public Conclusion Rule3(SubjectB subject)
{ Create conclusion }
}
[RuleSet(ContextB)]
public class RuleSet2
{
[Rule(TargetingB)]
public Conclusion Rule1(SubjectB subject)
{ Create conclusion }
[Rule(TargetingA)]
public Conclusion Rule2(SubjectA subject)
{ Create conclusion }
[Rule(TargetingB)]
public Conclusion Rule3(SubjectB subject)
{ Create conclusion }
}
public class Conclusion()
{
// Errorcode, Description and such
}
// contexts and targeting info are enums.
The goal is to create an extensible ruleset that doesn't alter the API from consumer POV while having good separation-of-concerns within the code files. Again: like a unit test framework.
I am trying to create a library of these that expose the following API
public static class RuleEngine
{
public static IEnumerable<IRuleSet> RuleSets(contextFlags contexts)
{
{
return from type in Assembly.GetExecutingAssembly().GetTypes()
let attribute =
type.GetCustomAttributes(typeof (RuleSetAttribute), true)
.OfType<RuleSetAttribute>()
.FirstOrDefault()
where attribute != null
select ?? I don't know how to convert the individual methods to Func's.
}
}
}
internal interface IRuleset
{
IEnumerable<Func<SubjectA, Conclusion>> SubjectARules { get; }
IEnumerable<Func<SubjectB, Conclusion>> SubjectBRules { get; }
}
...which allows consumers to simply use like this (using foreach instead of LINQ for readability in this example)
foreach (var ruleset in RuleEgine.RuleSets(context))
{
foreach (var rule in ruleset.SubjectARules)
{
var conclusion = rule(myContextA);
//handle the conclusion
}
}
Also, it would be very helpful if you could tell me how to get rid of "TargetingA" and "TargetingB" as RuleAttribute parameters and instead use reflection to inspect the parameter type of the decorated method directly. All the while maintaining the same simple external API.
You can use Delegate.CreateDelegate and the GetParameters method to do what you want.
public class RuleSet : IRuleSet
{
public IEnumerable<Func<SubjectA, Conclusion>> SubjectARules { get; set; }
public IEnumerable<Func<SubjectB, Conclusion>> SubjectBRules { get; set; }
}
public static class RuleEngine
{
public static IEnumerable<IRuleSet> RuleSets() // removed contexts parameter for brevity
{
var result = from t in Assembly.GetExecutingAssembly().GetTypes()
where t.GetCustomAttributes(typeof(RuleSetAttribute), true).Any()
let m = t.GetMethods().Where(m => m.GetCustomAttributes(typeof(RuleAttribute)).Any()).ToArray()
select new RuleSet
{
SubjectARules = CreateFuncs<SubjectA>(m).ToList(),
SubjectBRules = CreateFuncs<SubjectB>(m).ToList()
};
return result;
}
}
// no error checking for brevity
// TODO: use better variable names
public static IEnumerable<Func<T, Conclusion>> CreateFuncs<T>(MethodInfo[] m)
{
return from x in m
where x.GetParameters()[0].ParameterType == typeof(T)
select (Func<T, Conclusion>)Delegate.CreateDelegate(typeof(Func<T, Conclusion>), null, x);
}
Then you can use it like this:
var sa = new SubjectA();
foreach (var ruleset in RuleEngine.RuleSets())
{
foreach (var rule in ruleset.SubjectARules)
{
var conclusion = rule(sa);
// do something with conclusion
}
}
In your LINQ query you headed straight for RuleSetAttribute, and so lost other information. If you break the query in several lines of code you can get methods from the type with GetMethods(), and then you can call GetCustomAttribute<RuleAttribute>().
I would like to ignore certain properties when mapping deep (ie levels > 1) object models.
The following test works fine:
class Foo
{
public string Text { get; set; }
}
class Bar
{
public string Text { get; set; }
}
Mapper.CreateMap<Foo, Bar>()
.ForMember(x => x.Text, opts => opts.Ignore());
var foo = new Foo { Text = "foo" };
var bar = new Bar { Text = "bar" };
Mapper.Map(foo, bar);
Assert.AreEqual("bar", bar.Text);
However when I try to do the same mapping but have Foo and Bar as properties on a parent class the following test fails:
class ParentFoo
{
public Foo Child { get; set; }
}
class ParentBar
{
public Bar Child { get; set; }
}
Mapper.CreateMap<ParentFoo, ParentBar>();
Mapper.CreateMap<Foo, Bar>()
.ForMember(x => x.Text, opts => opts.Ignore());
var parentFoo = new ParentFoo
{
Child = new Foo { Text = "foo" }
};
var parentBar = new ParentBar
{
Child = new Bar { Text = "bar" }
};
Mapper.Map(parentFoo, parentBar);
Assert.AreEqual("bar", parentBar.Child.Text);
Instead of ignoring the text of the Child class (ie left it as "bar") automapper sets the value to null. What am I doing wrong with my mapping configuration?
There are two ways Automapper can perform the mapping. The first way is to simply give Automapper your source object and it will create a new destination object and populate everything for you. This is the way most apps use Automapper. However, the second way is to give it both a source and an existing destination and Automapper will update the existing destination with your mappings.
In the first example, you're giving it an existing destination value so Automapper will use that. In the second example, Automapper is going to do the mapping for ParentFoo and ParentBar, but when it gets to the Child, it's going to create a new Child and then do the mapping (this is the default behavior). This results in the Text property being null. If you want to use the existing Child object, you'll need to configure Automapper to do that with UseDestinationValue:
Mapper.CreateMap<ParentFoo, ParentBar>()
.ForMember(b => b.Child, o => o.UseDestinationValue());
This makes your test pass (as long as you get rid of the first space when setting the parentBar.Child.Text!).