c# - Enum description to string with AutoMapper - c#

I'm trying to project from my Order model to my OrderDTO model. Order has an enum. The problem is that projection doesn't work if I try to to get the Description attribute from the Enum. Here it's my code:
OrderStatus.cs:
public enum OrderStatus {
[Description("Paid")]
Paid,
[Description("Processing")]
InProcess,
[Description("Delivered")]
Sent
}
Order.cs:
public class Order {
public int Id { get; set; }
public List<OrderLine> OrderLines { get; set; }
public OrderStatus Status { get; set; }
}
OrderDTO.cs:
public class OrderDTO {
public int Id { get; set; }
public List<OrderLineDTO> OrderLines { get; set; }
public string Status { get; set; }
}
With this following configuration in my AutoMapper.cs:
cfg.CreateMap<Order, OrderDTO>().ForMember(
dest => dest.Status,
opt => opt.MapFrom(src => src.Status.ToString())
);
Projection works, but I get an OrderDTO object like this:
- Id: 1
- OrderLines: List<OrderLines>
- Sent //I want "Delivered"!
I don't want Status property to be "Sent", I want it to be as its associated Description attribute, in this case, "Delivered".
I have tried two solutions and none of them have worked:
Using ResolveUsing AutoMapper function as explained here, but, as it's stated here:
ResolveUsing is not supported for projections, see the wiki on LINQ projections for supported operations.
Using a static method to return the Description attribute in String by Reflection.
cfg.CreateMap<Order, OrderDTO>().ForMember(
dest => dest.Status,
opt => opt.MapFrom(src => EnumHelper<OrderStatus>.GetEnumDescription(src.Status.ToString()))
);
But this gives me the following error:
LINQ to Entities does not recognize the method 'System.String
GetEnumDescription(System.String)' method, and this method cannot be
translated into a store expression.
Then, how can I achieve this?

You can add an extension method like this one (borrowed the logic from this post):
public static class ExtensionMethods
{
static public string GetDescription(this OrderStatus This)
{
var type = typeof(OrderStatus);
var memInfo = type.GetMember(This.ToString());
var attributes = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
return ((DescriptionAttribute)attributes[0]).Description;
}
}
Then access it in your map:
cfg =>
{
cfg.CreateMap<Order, OrderDTO>()
.ForMember
(
dest => dest.Status,
opt => opt.MapFrom
(
src => src.Status.GetDescription()
)
);
}
This results in what you are asking for:
Console.WriteLine(dto.Status); //"Delivered", not "sent"
See a working example on DotNetFiddle
Edit1: Don’t think you can add a local look up function like that to LINQ to entities. It would only work in LINQ to objects. The solution you should pursue perhaps is a domain table in the database that allows you to join to it and return the column that you want so that you don’t have to do anything with AutoMapper.

You can achieve an expression based enum description mapping by building up an expression that evaluates to a string containing a condition statement (such as switch/if/case depending on how the provider implements it) with the enum descriptions as the results.
Because the enum descriptions can be extracted ahead of time we can obtain them and use them as a constant for the result of the condition expression.
Note: I've used the above extension method GetDescription() but you can use whatever flavour of attribute extraction you need.
public static Expression<Func<TEntity, string>> CreateEnumDescriptionExpression<TEntity, TEnum>(
Expression<Func<TEntity, TEnum>> propertyExpression)
where TEntity : class
where TEnum : struct
{
// Get all of the possible enum values for the given enum type
var enumValues = Enum.GetValues(typeof(TEnum)).Cast<Enum>();
// Build up a condition expression based on each enum value
Expression resultExpression = Expression.Constant(string.Empty);
foreach (var enumValue in enumValues)
{
resultExpression = Expression.Condition(
Expression.Equal(propertyExpression.Body, Expression.Constant(enumValue)),
// GetDescription() can be replaced with whatever extension
// to get you the needed enum attribute.
Expression.Constant(enumValue.GetDescription()),
resultExpression);
}
return Expression.Lambda<Func<TEntity, string>>(
resultExpression, propertyExpression.Parameters);
}
Then your Automapper mapping becomes:
cfg.CreateMap<Order, OrderDTO>().ForMember(
dest => dest.Status, opts => opts.MapFrom(
CreateEnumDescriptionExpression<Order, OrderStatus>(src => src.Status)));
When this is evaluated at runtime using Entity Framework with SQL server provider, the resulting SQL will be something like:
SELECT
-- various fields such as Id
CASE WHEN (2 = [Extent1].[Status]) THEN N'Delivered'
WHEN (1 = [Extent1].[Status]) THEN N'Processing'
WHEN (0 = [Extent1].[Status]) THEN N'Paid' ELSE N'' END AS [C1]
FROM [Orders] as [Extent1]
This should also work for other Entity Framework DB providers.

Related

Entity Framework Core 2.1 - owned types and nested value objects

I'm learning DDD and the tutorial I'm currently following is implemented using NHibernate, but since my lack of experience with it I've decided to go through the course using EF Core 2.1.
However, I'm currently a bit stuck with the following: I have three classes Customer which is an entity and two value objects (CustomerStatus and inside of it value object ExpirationDate) - like this:
public class Customer : Entity
{
//... constructor, other properties and behavior omitted for the sake of simplicity
public CustomerStatus Status { get; set; }
}
public class CustomerStatus : ValueObject<CustomerStatus>
{
// customer status is enum containing two values (Regular,Advanced)
public CustomerStatusType Type { get; }
public ExpirationDate ExpirationDate { get; }
}
public class ExpirationDate : ValueObject<ExpirationDate>
{
//... constructor, other properties and behavior omitted for the sake of simplicity
public DateTime? Date { get; private set; }
}
When I try to do the following inside of my DbContext:
modelBuilder.Entity<Customer>(table =>
{
table.OwnsOne(x => x.Status,
name =>
{
name.Property(x => x.ExpirationDate.Date).HasColumnName("StatusExpirationDate");
name.Property(x => x.Type).HasColumnName("Status");
});
});
I'm getting the following error:
The expression 'x => x.ExpirationDate.Date' is not a valid property expression. The expression should represent a simple property access: 't => t.MyProperty'.
Parameter name: propertyAccessExpression'
Beside this I've tried doing things like:
table.OwnsOne(x => x.Status.ExpirationDate,
name =>
{
name.Property(x => x.Date).HasColumnName("StatusExpirationDate");
});
table.OwnsOne(x => x.Status,
name =>
{
name.Property(x => x.Type).HasColumnName("Status");
});
But it also leads to:
The expression 'x => x.Status.ExpirationDate' is not a valid property expression. The expression should represent a simple property access: 't => t.MyProperty'.
I've also tried:
modelBuilder.Entity<Customer>()
.OwnsOne(p => p.Status,
cb => cb.OwnsOne(c => c.ExpirationDate));
But with no luck as well... Anyways, any help would be greatly appreciated, also if possible it would be really great if someone could explain why none of my tries are not working? Thanks in advance!
UPDATE
After doing as stated in Ivan's comment first I was getting error about CustomerStatus class constructor so I've added default protected one.
After that I started getting error:
Field 'k__BackingField' of entity type 'CustomerStatus' is readonly and so cannot be set.
Here's inner of my CustomerStatus class if it helps:
public class CustomerStatus : ValueObject<CustomerStatus>
{
public CustomerStatusType Type { get; }
public ExpirationDate ExpirationDate { get; }
public static readonly CustomerStatus Regular =
new CustomerStatus(CustomerStatusType.Regular, ExpirationDate.Infinite);
public bool IsAdvanced => Type == CustomerStatusType.Advanced && !ExpirationDate.IsExpired;
private CustomerStatus(CustomerStatusType type, ExpirationDate expirationDate)
{
Type = type;
ExpirationDate = expirationDate;
}
protected CustomerStatus()
{
}
public static CustomerStatus Create(CustomerStatusType type, ExpirationDate expirationDate)
{
return new CustomerStatus(type, expirationDate);
}
public CustomerStatus Promote()
{
return new CustomerStatus(CustomerStatusType.Advanced, ExpirationDate.Create(DateTime.UtcNow.AddYears(1)).Value);
}
protected override bool EqualsCore(CustomerStatus other)
{
return Type == other.Type && ExpirationDate == other.ExpirationDate;
}
protected override int GetHashCodeCore()
{
return Type.GetHashCode() ^ ExpirationDate.GetHashCode();
}
}
UPDATE
All it took was adding private setters on Type and ExpirationDate properties inside of CustomerStatus class and in combination with Ivan's answer it works like a charm. Thanks a lot!
Your attempts are not working because owned types can only be configured through their owner entity, and more specifically, through their own builder returned by the OwnsOne method or provided as an argument of the Action<T> argument of the OwnsOne method of the owner entity builder.
So the configuration should be something like this (note the nested OwnsOne):
modelBuilder.Entity<Customer>(customer =>
{
customer.OwnsOne(e => e.Status, status =>
{
status.Property(e => e.Type).HasColumnName("Status");
status.OwnsOne(e => e.ExpirationDate, expirationDate =>
{
expirationDate.Property(e => e.Date).HasColumnName("StatusExpirationDate");
});
});
});

How to Except Property instead of Select in Lambda LINQ

I have a code that fetching a table. Here's my sample code:
public IQueryable<MyItem> MyItems(){
return context.MyItem;
}
Here are the sample properties of MyItem
public int Id {get;set;}
public byte[] Image {get;set;}
public string Name {get;set;}
Since, byte[] can have multiple characters, I don't want to include then in searching because it will take so long if I have a records like 10,000 items.
Typically, I would Select like this:
public IQueryable<MyItem> MyItems(){
return context.MyItem.Select(item=>new MyItem{
Id=item.Id,
Name=item.Name
});
}
This is okay for few properties, but what I have a 10-20 properties, it would be hassle to write them one by one.
Is there any way like, I just Except the property Image in lambda for shorter code?
Create your own extension method SelectExcept:
public static IQueryable<T> SelectExcept<T, TKey>(this IQueryable<T> sequence,
Expression<Func<T, TKey>> excluder)
{
List<string> excludedProperties = new List<string>();
if (excluder.Body is MemberExpression memberExpression)
{
excludedProperties.Add(memberExpression.Member.Name);
}
else if (excluder.Body is NewExpression anonymousExpression)
{
excludedProperties.AddRange(anonymousExpression.Members.Select(m => m.Name));
}
var includedProperties = typeof(T).GetProperties()
.Where(p => !excludedProperties.Contains(p.Name));
return sequence.Select(x => Selector(x, includedProperties));
}
private static T Selector<T>(T obj, IEnumerable<PropertyInfo> properties)
{
var instance = Activator.CreateInstance<T>();
foreach (var property in properties)
property.SetValue(instance, property.GetValue(obj), null);
return instance;
}
Usage:
var result = context.MyItem.SelectExcept(x => x.Image);
You can exclude more than one property:
var result = context.MyItem.SelectExcept(x => new { x.Image, x.Name });
You can use AutoMapper for this. Just create a DTO class with all the properties excluding those which you don't want to query:
public class MyItemDTO
{
public int Id { get; set; }
public string Name { get; set; }
}
Then add some mapping:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<MyItem, MyItemDTO>();
});
Now you can query your entities like this:
context.MyItem
.OrderBy(m => m.Name)
.ProjectTo<MyItemDTO>();
This extension is available in AutoMapper.QueryableExtensions.
You can also create a DTO class with all the properties from an entity and then ignore some of them in mapping:
cfg.CreateMap<MyItem, MyItemDTO>().ForMember(d => d.Image, m => m.Ignore());
If you use EF Core you can instantiate entity class inside LINQ query so it's possible to map entity to itself and ignore some properties from it without creating additional DTO classes:
cfg.CreateMap<MyItem, MyItem>().ForMember(d => d.Image, m => m.Ignore());
Then you just use ProjectTo with the entity type which excludes unwanted properties from SQL query:
context.MyItem
.OrderBy(m => m.Name)
.ProjectTo<MyItem>();
Note that you may also need to create mappings for each navigation property of an entity.

Automapper expression mapping

I am trying to perform the following Automapper mapping for an OrderBy:
Expression<Func<ServerObject, object>> serverQueryable = x => x.TestEnumKVP.Value;
Mapper.Map<Expression<Func<ServerObject, object>>, Expression<Func<DatabaseObject, object>>(serverQueryable)
I want to map the ServerObject expression to a DatabaseObject expression
ServerObject defined as:
public class ServerObject
{
public KeyValuePairEx TestEnumKVP { get; set; }
}
KeyValuePairEx is a wrapper for the Enumeration which stores the Int16 value and the string value:
public enum TestEnum : Int16 { Test1, Test2, Test3 }
public class KeyValuePairEx
{
internal KeyValuePairEx(TestEnum key, string value) { }
public TestEnum Key { get; set; }
public string Value { get; set; }
}
DatabaseObject defined as:
public class DatabaseObject
{
public string TestEnumId { get; set; }
}
The Mapping I have is:
AutoMapper.Mapper.Initialize(config =>
{
config.CreateMap<DatabaseObject, ServerObject>().ForMember(dest => dest.TestEnumKVP.Value, opt => opt.MapFrom(src => src.TestEnumId));
});
The mapping fails with:
'Expression 'dest => dest.TestEnumKVP.Value' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.'
I need ServerObject.TestEnumKVP.Value to Map to DatabaseObject.TestEnumId. I am aware that Expression mappings are reversed - hence why the Map is from DatabaseObject to ServerObject. I have spent many hours on this and am at a loss as to how to get the mapping to work!
NB. I am using AutoMapper 6.1.1
Any help would be appreciated!
Thank you Lucian, I followed the github link and the solution offered by Blaise has worked. See below:
CreateMap<DatabaseObject, ServerObject>().ForMember(dest => dest.TestEnumKVP, opt => opt.MapFrom(src => src));
CreateMap<DatabaseObject, KeyValuePairEx>().ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.TestEnumId));
I was starting to look for at workarounds so delighted it was possible and that the solution was so clean and concise.
Thanks again!
The error and the solution are right there in the message. Forget about all the expression stuff. The ForMember is broken. Try ForPath instead.
Expression mapping now supports ForPath. See https://github.com/AutoMapper/AutoMapper/issues/2293.

Error When Querying For A Substring Using Dynamic Linq

I'm trying to use dynamic linq to obtain a subset of people from a database using Entity
Framework (EF). I'm running into a problem when using the contains operation. Here is the entity
for the People table:
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
Here is a query that works successfully.
var people = personContext
.People
.OrderBy("id asc")
.Skip(0)
.Take(5)
.ToList();
Notice that I'm using dynamic linq in the OrderBy method. However, when I try to apply
filtering, I get an exception.
var people = personContext
.People
.Where("id.Contains(15)")
.OrderBy("id asc")
.Skip(0)
.Take(5)
.ToList();
What I'd like to get back is a subset of people with ids that contain the substring "15", such as:
"015", "115", "151", "152", etc.
When I execute the code, I get the following error.
System.Linq.Dynamic.ParseException was unhandled by user code
Message=No applicable method 'Contains' exists in type 'String'
What is the syntax for determining if the Id field contains the string "15"?
What is the syntax for determining if the Id field contains the string "15"?
Well, definitely not .Where("id.Contains(15)") which is trying to invoke the method Contains with numeric value 15.
According to the documentation, you can use either a string literal:
.Where("id.Contains(\"15\")")
or substitution values:
.Where("id.Contains(#0)", "15")
I feel misconception here... You are not supposed to use LINQ like this.
As a start you need to invoke the overloads that accept lambdas; then you specify the property in the lambda and if its a string you invoke Contains on it. Like so:
var people = personContext
.People
.Where(p => p.Id.Contains("15"))
.OrderByDescending(p => p.Id)
.Skip(0) // You don't need this line.
.Take(5)
.ToList();
The EF itself will do the heavy lifting and translate these pure C# codes into the correct SQL statements.
You can't use Contains in the LINQ query. Instead you can try this
var people = (from p in personContext.Set<People>()
where p.Id.Contains("15")
orderby p.Id
select p).Skip(0).Take(5).ToList();

Exception "Specified method is not supported." being thrown from NHibernate IQueryable expression when using .Any extension

Utilizing NHibernate I am attempting to use a lambda expression to retrieve objects based on the status and values between a parent child relationship. AbstractWorkflowRequestInformation has a collection of WorkflowRequestInformationAction. Each of the two classes have their own Status properties. To illustrate here are the abbreviated classes as they relate to this query:
public class AbstractWorkflowRequestInformation
{
public virtual RequestStatus RequestStatus { get; set; }
public virtual IEnumerable<WorkflowRequestInformationAction>
WorkflowRequestInformationActionList { get; set; }
}
public class WorkflowRequestInformationAction
{
public virtual ActionStatus Status { get; set; }
public virtual string RoleIdentifier { get; set; }
public virtual string RoleName { get; set; }
}
Given this relationship I want to retrieve AbstractWorkflowRequestInformation objects based on a List<KeyValuePair<string, string>> called roles. I realize that the exception is being caused by a lack of parsing of the Any(...) extension method, but I am unsure of alternative queries. Thus far all permutations on the below query have caused the same or similar exceptions:
public IEnumerable<IRequestInformation> GetRequestsPendingActionBy(
List<KeyValuePair<string, string>> roles)
{
var results = GetSession().Query<AbstractWorkflowRequestInformation>()
.Where(r => r.RequestStatus == RequestStatus.Pending
&& r.WorkflowRequestInformationActionList
.Any(a => ActionStatus.Pending == a.Status
&& roles.Any(kp => kp.Key == a.RoleName
&& kp.Value == a.RoleIdentifier)))
.ToList();
return results;
}
The ultimate goal is to retrieve only those AbstractWorkflowRequestInformation objects which are pending and have a pending WorkflowRequestInformationAction matching a KeyValuePair in the roles enumerable.
I am not wedded to using a lambda expression as this expression has already grown unwieldy, if there's a more elegant ICriteria expression I am all ears. What are my options to restrict my results based upon the values in my roles List<KeyValuePair<string, string>> but prevent the "Specified method is not supported" exception?
I think this would get same results...
WorkflowRequestInformationAction actionAlias = null;
var q = GetSession().QueryOver<AbstractWorkflowRequestInformation>()
.Inner.JoinAlias(x => x.WorkflowRequestInformationActionList,
() => actionAlias)
.Where(x => x.RequestStatus == RequestStatus.Pending)
.And(() => actionAlias.Status == ActionStatus.Pending);
var d = Restrictions.Disjunction();
foreach(var kvp in roles)
{
d.Add(Restrictions.Where(() => actionAlias.RoleName == kvp.Key
&& actionAlias.RoleIdentitifier == kvp.Value));
}
q.And(d).TransformUsing(Transformers.DistinctRootEntity);
var results = q.List();
You could probably take a similar approach with NH Linq. I'm more comfortable with QueryOver/Criteria though.
The LINQ provider in NHibernate is not fully supported, you are trying to execute an extension method on a part of the expression tree that is not parsed from the provider.
This post might help you solve the problem. Be sure to checkout the related posts.
Also see the post from Fabio Maulo on NHibernate LINQ provider extension.

Categories