IncludeMembers() will always map from first match even if object is null.
Let's say we have the following source models:
public class Item
{
public MovieMetadata MovieMetadata { get; set; }
public BookMetadata BookMetadata { get; set; }
}
public class MovieMetadata
{
public string Title { get; set; }
}
public class BookMetadata
{
public string Title { get; set; }
}
Destination model:
public class ItemDetail
{
public string Title { get; set; }
}
Mapping profile:
public class ItemProfile : Profile
{
public ItemProfile()
{
CreateMap<Item, ItemDetail>()
.IncludeMembers(
src => src.BookMetadata,
src => src.MovieMetadata);
CreateMap<BookMetadata, ItemDetail>();
CreateMap<MovieMetadata, ItemDetail>();
}
}
And Program class with instantiation and testing logic:
public class Program
{
public static void Main()
{
//create mapper
var configuration = new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(ItemProfile));
});
var mapper = configuration.CreateMapper();
//check if configuration valid
mapper.ConfigurationProvider.AssertConfigurationIsValid();
Console.WriteLine("Mapper configuration is valid");
//try map book metadata
var bookItem = new Item
{
BookMetadata = new BookMetadata()
{
Title = "book"
},
MovieMetadata = null
};
var bookItemDetail = mapper.Map<ItemDetail>(bookItem);
bool isBookCorrectlyMapped = bookItem.BookMetadata.Title == bookItemDetail.Title;
Console.WriteLine($"Book mapped correctly: {isBookCorrectlyMapped}");
//try map movie metadata
var movieItem = new Item
{
BookMetadata = null,
MovieMetadata = new MovieMetadata()
{
Title = "movie"
}
};
var movieItemDetail = mapper.Map<ItemDetail>(movieItem);
bool isMovieCorrectlyMapped = movieItem.MovieMetadata.Title == movieItemDetail.Title;
Console.WriteLine($"Movie mapped correctly: {isMovieCorrectlyMapped}");
}
}
This is the output we will see:
Mapper configuration is valid
Book mapped correctly: True
Movie mapped correctly: False
We see that mapping for item with BookMetadata succeeded, but for MovieMetadata failed. Need to make updates so this test succeeds.
Suppose the reason it's failing is because of order of items in IncludeMembers():
.IncludeMembers(
src => src.BookMetadata,
src => src.MovieMetadata)
Here we have src.BookMetadata first and src.MovieMetadata second. During mapping it will find Title field in src.BookMetadata and will always use this value even if src.BookMetadata is null.
Is there any way to skip src.BookMetadata if it is null and use next src.MovieMetadata instead? Or probably need to use something else instead of IncludeMembers()?
Here is similar issue: https://github.com/AutoMapper/AutoMapper/issues/3204
Code above you can find here to quickly reproduce issue: https://dotnetfiddle.net/9YLGR6
Found possible solutions.
Option 1. IValueResolver
We can create custom value resolver. This solution is good for small amount of fields that we need to map from child objects. As any field will require specific value resolver.
Let's create TitleResolver:
public class TitleResolver : IValueResolver<Item, ItemDetail, string>
{
public string Resolve(Item source, ItemDetail destination, string destMember, ResolutionContext context)
{
if (source != null)
{
if (source.BookMetadata != null)
{
return source.BookMetadata.Title;
}
if (source.MovieMetadata != null)
{
return source.MovieMetadata.Title;
}
}
return null;
}
}
And update ItemProfile:
public class ItemProfile : Profile
{
public ItemProfile()
{
CreateMap<Item, ItemDetail>()
.ForMember(dest => dest.Title, opt => opt.MapFrom<TitleResolver>());
}
}
Link to whole code sample: https://dotnetfiddle.net/6pfKYh
Option 2. BeforeMap()/AfterMap()
In case if our child objects have more than one field to be mapped to destination object it may be a good idea to use BeforeMap() or AfterMap() methods.
In that case ItemProfile will be updated to:
public class ItemProfile : Profile
{
public ItemProfile()
{
CreateMap<Item, ItemDetail>(MemberList.None)
.AfterMap((src, dest, ctx) =>
{
if (src.BookMetadata != null)
{
ctx.Mapper.Map(src.BookMetadata, dest);
}
else if (src.MovieMetadata != null)
{
ctx.Mapper.Map(src.MovieMetadata, dest);
}
});
CreateMap<BookMetadata, ItemDetail>();
CreateMap<MovieMetadata, ItemDetail>();
}
}
Link to whole code sample: https://dotnetfiddle.net/ny1yRU
Related
I'm using AutoMapper to map my db models with my api models. But I have a problem with custom mapping. I'll try to explain my problem:
So I have the db and api models like this:
public class ApiModel
{
public List<ApiSubModel> Colors { get; set; }
}
public class DbModel
{
public string Type { get; set; }
public string Settings { get; set; }
}
Generally the DbModel Settings property is the serialized version of the ApiModel . So I want to achieve that with a custom convert when creating the maping:
Startup.cs:
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies())
Mapper profile class:
internal class DbToApiMapping : Profile
{
public DbToApiMapping()
{
CreateMap<ApiModel, DbModel>()
.ConvertUsing((source, dest, context) => new DbModel
{
Type = context.Items["Type"].ToString(),
Settings = JsonConvert.SerializeObject(source)
});
CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return new ApiModel
{
Colors = res.Colors
};
});
}
}
For the first map I use it like this:
var settings = modelMapper.Map<DbModel>(req.Settings, opt => opt.Items["Type"] = "Setpoint");
For the second map I use it like that:
var ss = modelMapper.Map<ApiModel>(settings.Settings);
The error I get when try to map is as follows:
Message:
AutoMapper.AutoMapperMappingException : Missing type map configuration or unsupported mapping.
Mapping types:
Object -> ApiModel
System.Object -> CommonLibrary.Models.ApiModel
I'm sure that I'm doing something wrong...but I can't quite catch what to look exactly. For the second mapping I tried with .ConvertUsing() method, but the error is the same.
Can someone help with this one.
Thanks in advance
Julian
---EDIT---
As suggested in the comments I tried without the DI. Here is the code:
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration( cfg =>
{
cfg.CreateMap<ApiModel, DbModel>()
.ConvertUsing((source, dest, context) => new DbModel
{
Type = context.Items["Type"].ToString(),
Settings = JsonConvert.SerializeObject(source)
});
cfg.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return new ApiModel
{
Colors = res.Colors
};
});
});
var modelMapper = config.CreateMapper();
var apiObj = new ApiModel
{
Colors = new List<ApiSubModel>
{
new ApiSubModel
{
SomeProp = "Test",
SubModel = new ApiSubSubModel
{
IntProp = 3,
StringProp = "Alabala"
}
}
}
};
DbModel dbRes = modelMapper.Map<DbModel>(apiObj, opt => opt.Items["Type"] = "Setpoint");
var dbObj = new DbModel
{
Type = "Setpoint",
Settings = "{\"Colors\":[{\"SomeProp\":\"Test\",\"SubModel\":{\"IntProp\":3,\"StringProp\":\"Alabala\"}}]}"
};
var apiRes = modelMapper.Map<ApiModel>(dbObj);
Console.WriteLine(dbRes.Settings);
Console.WriteLine(apiRes.Colors[0].SomeProp);
Console.WriteLine(apiRes.Colors[0].SubModel.StringProp);
Console.ReadLine();
}
}
public class ApiModel
{
public List<ApiSubModel> Colors { get; set; }
}
public class DbModel
{
public string Type { get; set; }
public string Settings { get; set; }
}
public class ApiSubModel
{
public string SomeProp { get; set; }
public ApiSubSubModel SubModel { get; set; }
}
public class ApiSubSubModel
{
public int IntProp { get; set; }
public string StringProp { get; set; }
}
It IS working, but there is something strange, when I want to debug the program and put a break point after var apiRes = modelMapper.Map<ApiModel>(dbObj); and try to debug the value of apiRes it says apiRes error CS0103: The name 'apiRes' does not exist in the current context
I tweaked your code a bit and now it works(although I had to use my own JSON and my own SubApiModel) but you can ask me about it in the comments if you're unsure
My models
public class ApiModel
{
public List<ApiSubModel> Colors { get; set; }
}
public class ApiSubModel
{
public string Name { get; set; }
}
public class DbModel
{
public DbModel(string type, string settings)
{
Type = type;
Settings = settings;
}
public string Type { get; set; }
public string Settings { get; set; }
}
and my JSON
{
Colors:
[
{
"Name": "AMunim"
}
]
}
and this is my mapping configuration:
.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return res;
});
This basically deserializes the whole JSON and since it has a property Color which is a list of ApiSubModels, I can simply convert the whole string to Object(of type ApiModel).
This is my complete testing code
using AutoMapper;
using Newtonsoft.Json;
using StackAnswers;
using StackAnswers.Automapper;
using System.Numerics;
DbModel dbModel = new DbModel("very important type", "{Colors: [{\"Name\": \"AMunim\"}]}");
MapperConfiguration config = new(cfg =>
{
cfg.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return res;
});
});
Mapper mapper = new(config);
ApiModel apiModel = mapper.Map<DbModel, ApiModel>(dbModel);
Console.WriteLine(apiModel.Colors.Count);
Console.WriteLine(apiModel.Colors.FirstOrDefault()?.Name);
Console.Read();
and the output:
EDIT
You can register your profiles/Mappings individually and force DI to use that i.e. register that
var config = new MapperConfiguration(c => {
//profile
c.AddProfile<DbToApiMapping>();
//mappings
c.CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return res;
});
});
//now register this instance
services.AddSingleton<IMapper>(s => config.CreateMapper());
Now when you request this service you will get the instance with configuration applied in your ctor
public class BadClass
{
private readonly IMapper _mapper;
BadClass(IMapper mapper)
{
_mapper = mapper;
}
public void HandleFunction()
{
//here you can use this
ApiModel apiModel = _mapper.Map<DbModel, ApiModel>(dbModel);
}
}
I managed to find my mistake, and I have to admit it is a silly one, but took me a lot of time to figure. The problem is in the line var ss = modelMapper.Map<ApiModel>(settings.Settings);
See I have the Profile like this:
CreateMap<DbModel, ApiModel>()
.ConstructUsing((source, context) =>
{
var res = JsonConvert.DeserializeObject<ApiModel>(source.Settings);
return new ApiModel
{
Colors = res.Colors
};
});
It expects the source to be a DbModel object, but in fact I pass a property of this object which is in fact a string. And I do not have defined that kind of mapping, that is why I get the error.
The right usage have to be: var ss = modelMapper.Map<ApiModel>(settings);
So thanks for all your suggestions!
Regards,
Julian
Question:
I have two classes (source and destination) with dto's which inherit from a abstract base class. When I try to map a list with source members to the destination dto's AutoMapper tries to map the missing mapping relation ship to the abstract destination class. How can I avoid this problem?
Information: It's not possible to make sure that all source members have their destination "partner".
These are the source member classes:
namespace Source
{
public abstract class Item
{
public int Id { get; set; }
public int Size { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class Asset1 : Item{}
public class Asset2 : Item{}
}
These are the destination member classes:
namespace Destination
{
public abstract class ItemDto
{
public int ProductId { get; set; }
public int Size { get; set; }
public string ProductName { get; set; }
public string Description { get; set; }
}
public class Asset1Dto : ItemDto{}
}
This is the AutoMapper Configuration:
namespace AutoMapper
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Item, ItemDto>(MemberList.Destination)
.Include<Asset1, Asset1Dto>()
.ForMember(o => o.ProductId, ex => ex.MapFrom(o => o.Id))
.ForMember(o => o.ProductName, ex => ex.MapFrom(o => o.Name))
.ReverseMap();
CreateMap<Asset1, Asset1Dto>(MemberList.Destination).ReverseMap();
}
}
}
This is the Main-Class:
namespace AutoMapperExample
{
public class Program
{
private static IList<Item> _items;
private static IMapper _mapper;
private static void Config()
{
_items = new List<Item>();
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
_mapper = new Mapper(config);
}
public static void Main(string[] args)
{
Config();
// Create dummy content
var rnd = new Random().Next();
_items.Add(new Asset1()
{
Name = typeof(Asset1).ToString(),
Id = rnd,
Size = 23,
Description = "Dummy-Text 1"
});
// working case
Console.WriteLine("Working when relation is present:");
try
{
var itemDto = _mapper.Map<IList<ItemDto>>(_items);
var jsonString = JsonSerializer.Serialize(itemDto);
Console.WriteLine(jsonString);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
Console.WriteLine();
rnd = new Random().Next();
_items.Add(new Asset2()
{
Name = typeof(Asset2).ToString(),
Id = rnd,
Size = 23,
Description = "Dummy-Text 2"
});
// case which throws the exception
Console.WriteLine("No destination member present:");
try
{
var itemDto = _mapper.Map<IList<ItemDto>>(_items);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
Exception I get:
---> System.ArgumentException: Cannot create an instance of abstract type ItemDto
Here is a gist repo with the example code.
The mapping for Asset2 doesn't work because you didn't configure it anywhere. You just need to add it, the same as with Asset1.
c.CreateMap<Item, ItemDto>()
.Include<Asset1, Asset1Dto>()
.Include<Asset2, Asset1Dto>()
.ForMember(o => o.ProductId, ex => ex.MapFrom(o => o.Id))
.ForMember(o => o.ProductName, ex => ex.MapFrom(o => o.Name))
.ReverseMap();
c.CreateMap<Asset1, Asset1Dto>().ReverseMap();
c.CreateMap<Asset2, Asset1Dto>(MemberList.Destination).ReverseMap();
#lucian-bargaoanu: According to your statement it's not possible to implement AutoMapper in a way to map the source class structure into the destination class structure (see image below) without getting an exception? If this statement is true, how can I solve the problem without adding a "Asst2Dto" class?
Thank you so much for your help!
Desired mapping operation:
var itemList = new List<Item>()
{
new Asset1(),
new Asset2(),
};
var itemDtoList = _mapper.Map<List<ItemDto>>(itemList);
Simplified source and target class diagram:
I have a need for customizing creation of a collection, with quite complicated relationships between the objects within it, and I can't figure out how to do it correctly.
For the sake of this issue, let's assume I'm working on a todo app. It has Items and SubItems, and the items have a week number indicating when they should be done:
public class Item {
public string Name { get; set; }
public int Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
}
public class SubItem {
public string Name { get; set; }
public Item Parent { get; set; }
}
Now, because this is what data usually looks like in the actual application, I want to create a collection of Items that has the following properties:
There are items that have the same name, but different weeks
There are items that have the same week but different name
There are sub-items that have the same name, but different parents
In order to do this, I've created a TodoItemSpecimenBuilder : ISpecimenBuilder which starts its Create method like this:
var type = (request as PropertyInfo)?.PropertyType ?? request as Type;
if (type == null || !typeof(IEnumerable<Item>).IsAssignableFrom(type))
{
return new NoSpecimen();
}
// build up the actual collection
return BuildActualCollection();
However, when I run tests with this specimen builder included in my context, I get lots (maybe 20 or 30) hits on the return statement before I enter even my setup code, and the first time I try to actually CreateMany<Item>(), it blows up with a cast exception because it can't cast OmitSpecimen to Item.
What am I doing wrong here?
Full sample code, compilable after installing NUnit and AutoFixture:
public class TodoList
{
public ICollection<Item> Tasks { get; set; }
}
public class Item
{
public string Name { get; set; }
public Week Week { get; set; }
public ICollection<SubItem> SubItems { get; set; }
public int ItemId { get; set; }
public TodoList TodoList { get; set; }
}
public class SubItem
{
public Item Item { get; set; }
public string Name { get; set; }
public int SortOrder { get; set; }
public string HelpText { get; set; }
}
public class Week
{
public int WeekId { get; set; }
}
public class ItemCollectionSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (!IsApplicable(request))
{
return new NoSpecimen();
}
var items = new List<Item>(3);
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week1));
items.Add(CreateItem(context, week2));
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
ConfigureSubItems(context, items);
return items;
}
private static bool IsApplicable(object request)
{
bool IsManyItemsType(Type type) => typeof(IEnumerable<Item>).IsAssignableFrom(type);
bool IsItemsType(Type type) => type != null && typeof(Item) == type;
switch (request)
{
case PropertyInfo pInfo:
return IsManyItemsType(pInfo.PropertyType);
case Type type:
return IsManyItemsType(type);
case MultipleRequest multipleRequest:
if (!(multipleRequest.Request is SeededRequest seededRequest))
{
return false;
}
return IsItemsType(seededRequest.Request as Type);
default:
return false;
}
}
private static Item CreateItem(ISpecimenContext context, Week week)
{
var item = context.Create<Item>();
item.Week = week;
return item;
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
{
name = item.Name;
}
else
{
item.Name = name;
}
}
}
private static void ConfigureSubItems(ISpecimenContext context, IEnumerable<Item> items)
{
foreach (var group in items.GroupBy(item => item.Week.WeekId))
{
var subItemTemplates = context.CreateMany<SubItem>().ToList();
foreach (var item in group)
{
item.SubItems.Clear();
foreach (var subItem in context.CreateMany<SubItem>().Zip(subItemTemplates,
(model, subItem) =>
{
subItem.Item = item;
subItem.Name = model.Name;
subItem.SortOrder = model.SortOrder;
subItem.HelpText = model.HelpText;
return subItem;
}))
{
item.SubItems.Add(subItem);
}
}
}
}
}
[TestFixture]
public class AutoFixtureSpecimenBuilderTests
{
private static void TestCreationOfTasks(Func<IFixture, ICollection<Item>> creator)
{
var fixture = new Fixture();
fixture.Customizations.Add(new ItemCollectionSpecimenBuilder());
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
var tasks = creator(fixture);
Assert.AreEqual(3, tasks.Count);
Assert.AreEqual(2, tasks.GroupBy(t => t.Week).Count());
Assert.IsTrue(tasks.GroupBy(t => t.Week).Select(g => g.Select(t => t.Name).Distinct()).All(distinctNames => distinctNames.Count() == 1));
var task = tasks.GroupBy(t => t.Week).OrderBy(g => g.Count()).First().OrderBy(t => t.ItemId).First();
}
[Test]
public void CreateMany() => TestCreationOfTasks(fixture => fixture.CreateMany<Item>().ToList());
[Test]
public void CreateWithProperty() => TestCreationOfTasks(fixture => fixture.Create<TodoList>().Tasks);
[Test]
public void CreateAsList() => TestCreationOfTasks(fixture => fixture.Create<IList<Item>>());
}
I can't think of any particularly good way to address this issue. The problem is that Item is a recursive (tree-like) data structure, and while AutoFixture does have some support for such, it's not easily extensible.
When you create an ISpecimenBuilder, you tell AutoFixture that this object is going to handle requests for particular objects. This means that you can no longer use the context to request those objects, because that'll recurse back into the same builder, causing an infinite recursion.
So, one option is to build up the objects 'by hand' from within the builder. You can still request all other types, but you'll have to avoid requesting objects that cause recursion.
Another option is to add a post-processor. Here's a proof of concept:
public class ItemCollectionSpecimenCommand : ISpecimenCommand
{
public void Execute(object specimen, ISpecimenContext context)
{
var #is = specimen as IEnumerable<Item>;
if (#is == null)
return;
var items = #is.ToList();
if (items.Count < 3)
return;
var week1 = context.Create<Week>();
var week2 = context.Create<Week>();
items[0].Week = week1;
items[1].Week = week1;
items[2].Week = week2;
items.GroupBy(t => t.Week).ToList().ForEach(ConfigureNames);
}
private static void ConfigureNames(IEnumerable<Item> items)
{
string name = null;
foreach (var item in items)
{
if (name == null)
name = item.Name;
else
item.Name = name;
}
}
}
You can configure your fixture like this:
var fixture = new Fixture();
fixture.Customizations.Add(
SpecimenBuilderNodeFactory.CreateTypedNode(
typeof(IEnumerable<Item>),
new Postprocessor(
new EnumerableRelay(),
new CompositeSpecimenCommand(
new AutoPropertiesCommand(),
new ItemCollectionSpecimenCommand()))));
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
.ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
This'll pass the repro tests CreateWithProperty and CreateAsList, but not CreateMany.
For various (historical) reasons, the way that CreateMany works is quite different from the way that something like Create<IList<>> works. If you really need this to work for CreateMany as well, I'll see what I can do, but I can't promise that this'll be possible at all.
After having looked at this repro for a few hours, this is the best I can come up with. I haven't really used AutoFixture for a year or two now, so it's possible that I'm simply out of shape, and that a better solution is available... I just can't think of it...
I want to create a custom attribute using Glass Mapper for getting the Sitecore URL, because it is not possible to lazy load a property with SitecoreInfo(SitecoreInfoType.Url) and we have some performance issues loading URL of mapped items, where the URL will never be used.
Here is what I've got so far:
The Configuration
public class SitecoreUrlConfiguration : AbstractPropertyConfiguration
{
public SitecoreInfoUrlOptions UrlOptions { get; set; }
public bool IsLazy { get; set; }
}
The Attribute
public class SitecoreUrlAttribute : AbstractPropertyAttribute
{
public SitecoreUrlAttribute()
{
this.IsLazy = true;
this.UrlOptions = SitecoreInfoUrlOptions.Default;
}
/// <summary>
/// Gets or sets a value indicating whether is lazy.
/// </summary>
public bool IsLazy { get; set; }
public SitecoreInfoUrlOptions UrlOptions { get; set; }
public override AbstractPropertyConfiguration Configure(PropertyInfo propertyInfo)
{
var config = new SitecoreUrlConfiguration();
this.Configure(propertyInfo, config);
return config;
}
public void Configure(PropertyInfo propertyInfo, SitecoreUrlConfiguration config)
{
config.UrlOptions = this.UrlOptions;
config.IsLazy = this.IsLazy;
base.Configure(propertyInfo, config);
}
}
The Mapper
public class SitecoreUrlMapper : AbstractDataMapper
{
public override object MapToProperty(AbstractDataMappingContext mappingContext)
{
var context = mappingContext as SitecoreDataMappingContext;
if (context == null)
{
throw new MapperException("Mapping Context is null");
}
var item = context.Item;
var scConfig = this.Configuration as SitecoreUrlConfiguration;
if (scConfig == null)
{
throw new MapperException("SitecoreUrlConfiguration is null");
}
var urlOptions = Utilities.CreateUrlOptions(scConfig.UrlOptions);
urlOptions.Language = null;
// now, what?
}
}
So far, so good. But how can I lazy load the URL in the mapper? Does anyone have an idea?
The only way I actually see is to map a Lazy<T> and add a new property to the class which returns the value of this when accessing it. So in you mapper, where you put // now what? I would return the lazy string:
return new Lazy<string>(() => LinkManager.GetItemUrl(item, urlOptions));
Then in your model, put these two properties:
[SitecoreUrl]
public Lazy<string> LazyUrl { private get; set; }
[SitecoreIgnore]
public virtual string Url
{
get
{
return this.LazyUrl.Value;
}
}
You can achieve pretty similar to this with a bit of creativity and the new Delegate functionality
In the fluent configuration map the type like so:
SitecoreType<IWhatever> sitecoreType = new SitecoreType<IWhatever>();
sitecoreType.Delegate(y => y.Url).GetValue(GetLazyUrl);
private LazyString GetLazyUrl(SitecoreDataMappingContext arg)
{
var item = context.Item;
return new LazyString(
() =>
{
// the necessary actions to get the url
});
}
public class LazyString : Lazy<string>
{
public LazyString(Func<string> valueFactory) : base(valueFactory)
{
}
public override string ToString()
{
return Value;
}
public static implicit operator string(LazyString lazyString)
{
return lazyString.Value;
}
}
It's not a string, but for the purposes of many applications, will behave like one.
With reference to the 2 classes below, I am regularly writing LINQ statements like this..
using (var db = new DBContext())
{
var result = db.Countries
.Select(c => new
{
c.Name,
c.Leader != null ? c.Leader.Title : String.Empty,
c.Leader != null ? c.Leader.Firstname : String.Empty,
c.Leader != null ? c.Leader.Lastname : String.Empty
});
}
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public Leader Leader { get; set; }
}
public class Leader
{
public string Title { get; set; }
public string Firstname { get; set; }
public string Lastname { get; set; }
}
My issues that I am having to constantly repeat my null checks on children navigation properties and I was wondering if there is a way I can use some kind of expression tree to extract the property values dynamically whilst checking for null values and if they didn't exist send back an empty string, something like the method below..
public class Country
{
// Properties //
public string SafeGet(Expression<Func<Country, string>> fnc)
{
// Unpack fnc and check for null on each property?????
}
}
Usage:
using (var db = new DBContext())
{
var result = db.Countries
.Select(c => new
{
c.Name,
c.SafeGet(l => l.Leader.Title),
c.SafeGet(l => l.Leader.Firstname),
c.SafeGet(l => l.Leader.Lastname)
});
}
If someone could provide a basic example that would be great as I don't have a whole lot of experience with expression tree's other than creating them.
Thanks.
Update -> would something like the following work?
public string GetSafe(Expression<Func<Country, string>> fnc)
{
var result = fnc.Compile().Invoke(this);
return result ?? string.Empty;
}
I see no need for an expression. I would simply go for an extension-method like
public static class ModelExtensions
{
// special case for string, because default(string) != string.empty
public static string SafeGet<T>(this T obj, Func<T, string> selector)
{
try {
return selector(obj) ?? string.Empty;
}
catch(Exception){
return string.Empty;
}
}
}
It works for all classes and you could implement further for other datatypes. The usage is the same as yours.
I think that you want something like this:
public static class ModelExtensions
{
public static TResult SafeGet<TSource, TResult>(this TSource obj, System.Func<TSource, TResult> selector) where TResult : class
{
try
{
return selector(obj) ?? default(TResult);
}
catch(System.NullReferenceException e)
{
return default(TResult);
}
}
}