I would like to retrieve a nested object from documents in my index called "userprofiles".
My UserProfile model:
public class UserProfileModel
{
public string FullName { get; set; }
public string Oid { get; set; }
public string Upn { get; set; }
public List<SsoLink> FavoriteSsoLinks { get; set; } = new List<SsoLink>();
}
My SsoLink Model:
public class SsoLink
{
public string Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string Owner { get; set; }
}
Index creation:
PUT userprofiles
{
"mappings" : {
"properties" : {
"FavoriteSsoLinks" : {
"type" : "object"
}
}
}
}
My Query:
var searchResponse = _client.Search<UserProfileModel>(s => s
.Index(_profileIndex)
.Query(q=>q
.Term(t => t.Field(t => t.Oid).Value(oid)
)
)
);
Right now it returns the documents, but the favoritelinks object is blank, however I see objects listed from Kibana. I must be missing something obvious, but having trouble figuring this out.
Here is an example of my data:
Index creation example uses "FavoriteSsoLinks" as the property, but the Kibana screenshot uses "favoriteSsoLinks" starting with a lowercase f - which is correct? My suspicion is that property casing may be the issue, but would need to see the mapping in the index to know if this is correct.
The 7.x client is strict about property name casing in JSON, and by default uses camelcase property names to match to POCO property names. For example, by default
"favoriteSsoLinks" in JSON will be a match for FavoriteSsoLinks POCO property
"FavoriteSsoLinks" in JSON will not be a match for FavoriteSsoLinks POCO property
This behaviour can be changed with DefaultFieldNameInferrer(Func<string, string>) on ConnectionSettings for all properties, or on a property by property basis using attributes on properties such as DataMemberAttribute or PropertyNameAttribute, or using DefaultMappingFor<T> where T is the POCO type.
Related
Suppose you have this source model:
public abstract class SourceModelBase {
}
public class SourceContact : SourceModelBase {
public string FirstName { get; set; }
public string LastName { get; set; }
public KeyValuePair Pair { get; set; }
public SourceAddress Address { get; set; }
}
public class KeyValuePair { // Not derived from SourceModelBase.
public string Key { get; set; }
public string Value { get; set; }
}
public class SourceAddress : SourceModelBase {
public string StreetName { get; set; }
public string StreetNumber { get; set; }
}
Now the destination model should be mapped 1:1 by default (subject to normal AutoMapper configuration), but each thing derived from SourceModelBase should be mapped to a wrapper class class Wrap<T> { T Payload { get; set; } string Meta { get; set; } }.
public abstract class DestinationModelBase {
}
public class DestinationContact : DestinationModelBase {
public string FirstName { get; set; }
public string LastName { get; set; }
public KeyValuePair Pair { get; set; } // Not wrapped, base class not `SourceModelBase`.
public Wrap<DestinationAddress> Address { get; set; }
}
public class DestinationAddress : DestinationModelBase {
public string StreetName { get; set; }
public string StreetNumber { get; set; }
}
Since the contact class itself is derived from SourceModelBase it should be wrapped as well.
The result should have this structure:
Wrap<DestinationContact> Contact
string Meta // Comes from the custom wrapper logic.
DestinationContact Payload
string FirstName
string LastName
KeyValuePair Pair
string Key
string Value
Wrap<DestinationAddress> Address
string Meta // Comes from the custom wrapper logic.
DestinationAddress Payload
string StreetName
string StreetNumber
Obviously this wrapping should nest, illustrated by the fact that the mapped object itself is subject to it and so is its Address property.
For some reason all I keep finding are questions related to mapping from destination to source. I know I have to somehow use ResolveUsing and if the destination type is derived from SourceModelBase, somehow apply custom logic to provide the Wrap<T> value based on the value of the source property.
I don't know where to start at all, though. Especially when the source object itself is specified to be subject of the wrapping logic as well.
What's the best, most AutoMapper-idiomatic way to wrap the nested objects if they meet a condition and at the same time wrap the original object as well if it meets the same condition? I already have the mapper creation abstracted away so I can mold the original object automatically before passing it to the mapper, which may help with subjecting the original object to the resolver as well by doing mapper.Map(new { Root = originalObject }) so the resolver sees the instance of the original object as if it was a value of a property of source object as well, not the source object itself.
According to this issue on AutoMapper GitHub page, there is no direct way to do it.
But there is some workarounds. For example - reflection.
In this case you need to know wrapper type and implement converter for desired types. In this example it's MapAndWrapConverter from TSource to Wrap<TDestination>
CreateWrapMap method creates two bindings:
SourceAddress -> Wrap<DestinationAddress> and SourceContact -> Wrap<DestinationContact> which allow you to map SourceContant to wrapped DestinationContact.
internal class Program
{
public static void Main()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<SourceAddress, DestinationAddress>();
cfg.CreateMap<SourceContact, DestinationContact>();
cfg.CreateWrapMap(
//func selecting types to wrap
type => typeof(DestinationModelBase).IsAssignableFrom(type)
&& !type.IsAbstract,
typeof(Wrap<>),
typeof(MapAndWrapConverter<,>));
});
var mapper = config.CreateMapper();
//Using AutoFixture to create complex object
var fixture = new Fixture();
var srcObj = fixture.Create<SourceContact>();
var dstObj = mapper.Map<Wrap<DestinationContact>>(srcObj);
}
}
public static class AutoMapperEx
{
public static IMapperConfigurationExpression CreateWrapMap(
this IMapperConfigurationExpression cfg,
Func<Type, bool> needWrap, Type wrapperGenericType,
Type converterGenericType)
{
var mapperConfiguration =
new MapperConfiguration((MapperConfigurationExpression)cfg);
var types = Assembly.GetExecutingAssembly().GetTypes();
foreach (var dstType in types.Where(needWrap))
{
var srcType = mapperConfiguration.GetAllTypeMaps()
.Single(map => map.DestinationType == dstType).SourceType;
var wrapperDstType = wrapperGenericType.MakeGenericType(dstType);
var converterType = converterGenericType.MakeGenericType(srcType, dstType);
cfg.CreateMap(srcType, wrapperDstType)
.ConvertUsing(converterType);
}
return cfg;
}
}
public class MapAndWrapConverter<TSource, TDestination>
: ITypeConverter<TSource, Wrap<TDestination>>
{
public Wrap<TDestination> Convert(
TSource source, Wrap<TDestination> destination, ResolutionContext context)
{
return new Wrap<TDestination>
{
Payload = context.Mapper.Map<TDestination>(source)
};
}
}
CreateWrapMap method is a little bit messy, especially the part with finding matching types. But it can be refined according to your needs.
I have two classes being mapped. The source class has a DateTime property that gets mapped to a destination property of type long (DateTime.Ticks), they are UpdateDt and UpdateDtTicks respectively.
When I use Automapper to map these two classes my UpdateDtTicks property automaticly gets the value from the UpdateDt property, even though the property names are not the same and I have not explicitly set the mapping for this property.
Is Automapper setting the property automatically because the property names only differ at the end? If not why is this happening as it is unexpected behavior.
Please see the code below:
static void Main(string[] args)
{
Configuration.Configure();
var person = new OrderDto
{
OrderId = 999,
MyDate = new DateTime(2015, 1, 1, 4, 5, 6)
};
var orderModel = Mapper.Map<OrderModel>(person);
Console.WriteLine(new DateTime(orderModel.MyDateTicks.Value));
Console.ReadLine();
}
public class OrderDto
{
public int OrderId { get; set; }
public DateTime MyDate { get; set; }
}
public class OrderModel
{
public int OrderId { get; set; }
public long? MyDateTicks { get; set; }
}
public class Configuration
{
public static void Configure()
{
Mapper.CreateMap<OrderDto, OrderModel>();
}
}
The result in the console:
And a watch:
You are triggering AutoMapper's flattening feature. Frome the documentation:
When you configure a source/destination type pair in AutoMapper, the configurator attempts to match properties and methods on the source type to properties on the destination type. If for any property on the destination type a property, method, or a method prefixed with "Get" does not exist on the source type, AutoMapper splits the destination member name into individual words (by PascalCase conventions).
Thus, given the following example (from their docs as well, shortened in my answer):
public class Order
{
public Customer Customer { get; set; }
}
public class Customer
{
public string Name { get; set; }
}
public class OrderDto
{
public string CustomerName { get; set; }
public decimal Total { get; set; }
}
Mapper.CreateMap<Order, OrderDto>();
var order = new Order
{
Customer = new Customer
{
Name = "John Doe"
}
};
OrderDto dto = Mapper.Map<Order, OrderDto>(order);
The CustomerName property matched to the Customer.Name property on Order.
This is exactly the same as MyDateTicks matching MyDate.Ticks, which returns a long as necessary...
I have created below object which will be mapped to ElasticSearch type. I would like to exclude the UnivId property from being indexed:
[ElasticType(Name = "Type1")]
public class Type1
{
// To be ignored
public string UnivId { get; set; }
[ElasticProperty(Name="Id")]
public int Id { get; set; }
[ElasticProperty(Name = "descSearch")]
public string descSearch { get; set; }
}
You should be able to set the OptOut value of the ElasticProperty attribute, like the following:
[ElasticProperty(OptOut = true)]
public string UnivId { get; set; }
In NEST 2.0 ElasticPropertyAttribute is replaced by per type attributes (StringAttribute, DateAttribute...). I used Ignore parameter to exclude property.
Exemple for string :
[String(Ignore = true)]
public string Id {get;set;}
If using Nest 5.0+, per the docs several ways to ignore a field:
Ignore attribute should work:
using Nest;
[Ignore]
public string UnivId { get; set; }
JsonIgnore can also be used since Newtonsoft.Json is the default serializer used by Nest.
Another way is to use type specific attribute mappings associated with the property. For example, since it's a string then use Text attribute:
[Text(Ignore = true)]
public string UnivId { get; set; }
or if an int use Number:
[Number(Ignore = true)]
public int Id { get; set; }
In addition, instead of using an explicit attribute on the property, the mapping can be ignored using .DefaultMappingFor<... on ConnectionSettings (see docs for more detail)
var connectionSettings = new ConnectionSettings()
.DefaultMappingFor<Type1>(m => m.Ignore(p => p.UnivId);
However, if want to conditionally ignore an attribute if the value is null then use the following Newtonsoft.Json attribute with null handling setting:
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string UnivId { get; set; }
I found the above useful when doing partial updates on a doc but wanted to re-use the same C# class for indexing and avoid overwriting existing values in the index.
I have the following classes (overly simplified):
public class Person
{
public int ID { get; set; }
}
public class Content
{
public int ID { get; set; }
}
public class Image : Content
{
public bool Private { get; set; }
public Person Author { get; set; }
}
public class Tag
{
public int ID { get; set; }
public Content Content { get; set; }
public Person Person { get; set; }
}
I'd like to get all the Tags where the Content is an Image and the Image is not Private (while eagerly loading a property of Image). Example that attempts to do this but doesn't work:
var tags = context.Tags
.Include("Content.Author")
.Include("Person")
.Where(t => !((Image)t.Content).Private);
I get the following errors:
Unable to cast the type 'Content' to type 'Image'. LINQ to Entities only supports casting EDM primitive or enumeration types.
And with the Where clause removed:
A specified Include path is not valid. The EntityType 'Content' does not declare a navigation property with the name 'Author'.
What kind of query and/or model schema change would I need to be able to accomplish this approach?
You can write the filter in the Where clause the following way:
.Where(t => t.Content is Image && !(t.Content as Image).Private)
However, the bigger problem is the Include part. The Author property only exists for the derived type Image but Include will try to load the base type Content (which doesn't have an Author property) because that's the type of the navigation property Content in Tag. You just can't use Include here.
You can try to rewrite the query as a projection:
var tags = context.Tags
.Where(t => t.Content is Image && !(t.Content as Image).Private)
.Select(t => new
{
Tag = t,
Image = t.Content as Image, // possibly this line is not needed
Author = (t.Content as Image).Author,
Person = t.Person
})
.AsEnumerable()
.Select(x => x.Tag)
.ToList();
As long as you don't disable change tracking (with AsNoTracking for example) EF should put the object graph together automatically so that the loaded tags have a populated Content, Content.Author and Person property (as if you had loaded the navigation properties with Include).
BTW: The feature to include navigation properties of a derived type is requested here on UserVoice. It's not exactly the same as your situation, but in the comment section is a request even for your scenario.
Try changing the class definitions to something like...
class Person
{
public int ID { get; set; }
}
class Content
{
public int ID { get; set; }
}
class Image : Content
{
public bool IsPrivate { get; set; }
public virtual Person Author { get; set; }
}
class Tag
{
public int ID { get; set; }
public Content Content { get; set; }
public Person Person { get; set; }
}
Private doesnt seem like a good name for a property because it conflicts with the public or private declaration.
Given the following class....
namespace IMTool.Data
{
public partial class AllContracts
{
internal class Metadata
{
public int ContractId { get; set; }
[Required]
public string Name { get; set; }
}
}
}
and given the following.
using (var context = new IMToolDataContext())
{
ddlContracts.DataValueField = "ContractId";
ddlContracts.DataTextField = "Name";
ddlContracts.DataSource = context
.AllContracts
.OrderBy(o => o.Name);
ddlContracts.DataBind();
}
How can I strongly type the dropdown list DataValue and the DataText fields? Basically I do not want to use a string but rather the column name from the entity, I am using LinqToSql (well PLinqo which is a codesmith set of templates to generate my datalayer)
Can anyone help?
Create a custom attribute to do this
internal class Metadata
{
[MappingColumn (Type="Key")]
public int ContractId { get; set; }
[Required]
[MappingColumn (Type="Name")]
public string Name { get; set; }
}
create two methods with this signature
string GetKeyColumName(Type type) //will perfom a loop on the type properties custom attribute and return the first with the type Key
string GetNameColumnName(Type type)//will perfom a loop on the type properties custom attribute and return the first with the type Name
and populate your ddl like this:
using (var context = new IMToolDataContext())
{
ddlContracts.DataValueField = GetKeyColumnName(typeof(Metadata));
ddlContracts.DataTextField = GetNameColumnName(typeof(Metadata));
ddlContracts.DataSource = context
.AllContracts
.OrderBy(o => o.Name);
ddlContracts.DataBind();
}
EDIT:
The column attribute I refer is yourcunstom attribute, not the one from Linq. Ok I should have called it MappingColumn it can be declare like this:
public class MappingColumnAttribute : System.Attribute
{
public string Type {get;set;}
}