AutoMapper expression mapping with ValueObject - c#

I have a business logic object User with ValueObject field UserId. I need to translate expression tree with User to similar tree with DbUser using AutoMapper.
Here is my setup:
public static void Main(string[] args)
{
// create new AutoMapper
var mapperConfiguration = new MapperConfiguration(
cfg =>
{
cfg.CreateMap<UserId, string>().ConvertUsing(u => u.ToString());
cfg.CreateMap<string, UserId>().ConvertUsing(u => UserId.FromString(u));
cfg.CreateMap<User, DbUser>()
.ForMember(dbu => dbu.Id, opt => opt.MapFrom(u => u.Id.ToString()));
cfg.CreateMap<DbUser, User>()
.ForMember(u => u.Id, opt => opt.MapFrom(dbu => UserId.FromString(dbu.Id)));
cfg.AddExpressionMapping();
}
);
mapperConfiguration.AssertConfigurationIsValid();
IMapper mapper = mapperConfiguration.CreateMapper();
// map some expression
var idToFind = new UserId(Guid.NewGuid());
Expression<Func<User, bool>> bllExpression = x => x.Id == idToFind;
var dbExpression = mapper.MapExpression<Expression<Func<DbUser, bool>>>(bllExpression);
Console.WriteLine(dbExpression.ToString());
}
User and DbUser:
public class User
{
public UserId Id { get; set; }
public string Name { get; set; }
}
public class DbUser
{
public string Id { get; set; }
public string Name { get; set; }
}
UserId:
public class UserId
{
public Guid Id { get; }
public UserId(Guid id) => Id = id;
public override string ToString()
{
return Id.ToString();
}
public static UserId FromString(string value)
{
return new UserId(Guid.Parse(value));
}
public static bool operator ==(UserId a, UserId b)
{
return a?.Id == b?.Id;
}
public static bool operator !=(UserId a, UserId b)
{
return a?.Id != b?.Id;
}
}
So, when I convert those expressions I get this result:
x => (FromString(x.Id) == 799e50f4-339f-4207-819e-194bbe206ae2)
But I cannot use this expression as database query e.g. in EntityFramework or MongoDb. Because there is no FromString function. Instead I need to compare already flattened values, in this case string:
x => (x.Id == "799e50f4-339f-4207-819e-194bbe206ae2")
How can I get this expression?
Link to .NET Fiddle

Related

EF Core union with value conversion

I want my union LINQ query to be evaluated on server side with EF Core.
There're entities:
public class Entity1
{
public int Id { get; set; }
public List<StringWithStyle> Names { get; set; } = new List<StringWithStyle>();
}
public class Entity2
{
public int Id { get; set; }
public StringWithStyle Name { get; set; }
}
public class StringWithStyle
{
public string Text { get; set; }
public bool IsBold { get; set; }
public bool IsItalic { get; set; }
public bool IsUpperCase { get; set; }
}
Their properties are stored in DbContext as json string using Value conversion:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Entity1>()
.HasKey(e => e.Id);
modelBuilder.Entity<Entity1>()
.Property(e => e.Names)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<StringWithStyle>>(v, (JsonSerializerOptions)null)
,
new ValueComparer<List<StringWithStyle>>(
(arr1, arr2) => arr1.Count() == arr2.Count() && !arr1.Except(arr2).Any(),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())))
);
modelBuilder.Entity<Entity2>()
.HasKey(e => e.Id);
modelBuilder.Entity<Entity2>()
.Property(e => e.Name)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<StringWithStyle>(v, (JsonSerializerOptions)null)
,
new ValueComparer<StringWithStyle>(
(val1, val2) => val1.Equals(val2),
c => c.GetHashCode())
);
}
I need to show both entities in one grid. So, I use such a query:
var entities1 = from e1 in dbContext.Set<Entity1>()
select new GridModel
{
Id = e1.Id,
IsFirst = true,
Names = e1.Names,
Name = default
};
var entities2 = from e2 in dbContext.Set<Entity2>()
select new GridModel
{
Id = e2.Id,
IsFirst = false,
Name = e2.Name,
Names = default
};
var grid = entities1.Union(entities2).ToList();
And it throws an Exception:
System.InvalidOperationException : Unable to translate set operation after client projection has been applied. Consider moving the set operation before the last 'Select' call.
Is it possible to to get such a query that is evaluating on server side?
*** UPDATE ***
There's GridModel class:
public class GridModel
{
public int Id { get; set; }
public bool IsFirst { get; set; }
public List<StringWithStyle> Names { get; set; }
public StringWithStyle Name { get; set; }
}

What's the best way to test with AutoFixture a function that uses a "Mapper"

I have written an application which communicates with Auth0, using the official Auth0 NuGet package.
The function in the Auth0 NuGet package which I call returns a collection of Users.
I map this collection to a domain object using an extension method:
NOTE: This class is an internal, class, it's not exposed to the outside world and it shouldn't be.
public static IEnumerable<UserModel> MapToUserModel(this IEnumerable<User> users)
{
return users.Select(
static user => new UserModel
{
Id = user.UserId,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
AvatarLink = user.Picture,
LastSeen = user.LastLogin?.ToUnixTime(),
Status = UserExtensions.GetStatus(user),
});
}
private static UserStatus GetStatus(UserBase user)
{
if (!user.EmailVerified.HasValue || !user.EmailVerified.Value)
{
return UserStatus.Inactive;
}
if (user.Blocked.HasValue && user.Blocked.Value)
{
return UserStatus.Pending;
}
if (!user.Blocked.HasValue || user.Blocked.HasValue && !user.Blocked.Value)
{
return UserStatus.Active;
}
return UserStatus.Inactive;
}
Here's the relevant domain model:
public sealed class UserModel
{
public string? Id { get; init; }
public string? Email { get; init; }
public string? FirstName { get; init; }
public string? LastName { get; init; }
public string? AvatarLink { get; init; }
public long? LastSeen { get; init; }
public UserStatus Status { get; init; }
}
Now, I do have a function which communicates with Auth0 to retrieve a collection of users, and each of these users is mapped to a UserModel instance.
But now I am facing issues with how to test this properly.
First of all, unit tests should only be testing the public API, so the mapper itself should be tested through the public API.
A simplified test looks like:
[Fact]
public void SimpleTest()
{
// ARRANGE.
var fixture = new Fixture();
var auth0Users = fixture.CreateMany<User>();
// NOTE: Here the function is called which in turns calls the mapping.
var results =
// ASSERT.
results.Should()
.HaveCount(auth0Users.Count)
.And.BeEquivalentTo(
auth0Users.Select(
static (user, _) => new UserModel
{
Id = user.UserId,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
AvatarLink = user.Picture,
LastSeen = CalculateLastSeen(user.LastLogin),
Status = CalculateUserStatus(user),
}),
static options => options.Including(static x => x.Id)
.Including(static x => x.Email)
.Including(static x => x.FirstName)
.Including(static x => x.LastName)
.Including(static x => x.AvatarLink)
.Including(static x => x.LastSeen)
.Including(static x => x.Status));
// HELPER FUNCTIONS.
static long? CalculateLastSeen(DateTime? dateTime)
{
if (dateTime == null)
{
return null;
}
return new DateTimeOffset(((DateTime)dateTime).ToUniversalTime()).ToUnixTimeSeconds();
}
static UserStatus CalculateUserStatus(UserBase user)
{
if (!user.EmailVerified.HasValue || !user.EmailVerified.Value)
{
return UserStatus.Inactive;
}
if (user.Blocked.HasValue && user.Blocked.Value)
{
return UserStatus.Pending;
}
if (!user.Blocked.HasValue || user.Blocked.HasValue && !user.Blocked.Value)
{
return UserStatus.Active;
}
return UserStatus.Inactive;
}
}
Of course, AutoFixture doesn't generate permutations of each possible value for EmailVerified and User.Blocked.
How should I test this with AutoFixture? Is something wrong with the design and should it be improved?
If not, how can I configure AutoFixture to ensure that each permutation is going to be present in the collection?

Automapper Complex mapping from List to Object

I have object structure as below
public class Source
{
public Connection Connection { get; set;}
}
public class Connection
{
public string Id { get; set;
public IEnumerable<Distributor> Distributors { get; set;
}
public class Distributor { get; set; }
{
public DistributorType Type {get; set;}
public string X { get; set; }
public string Y { get; set; }
}
public class Destination
{
public Distribution Distribution { get; set;
}
public class Distribution
{
public string Id { get; set;}
public string X { get; set; }
public string Y { get; set; }
}
I would like to map the Source to Destination for the property Distribution. The mapping is as below
Source.Connection.Distributors.FirstOrDefault().X => Destination.Distribution.X
Source.Connection.Distributors.FirstOrDefault().Y => Destination.Distribution.Y
Source.Connection.Id => Destination.Distribution.Id
I have tried using the Custom Resolver but no luck
public class CustomDistributorResolver : IValueResolver<Source, Destination, Distribution >
{
public Distribution Resolve(Source source, Destination destination, Distribution destMember, ResolutionContext context)
{
var result = source.Connection.Distributors.FirstOrDefault(x => x.DistributorType =="ABC");
if (result == null) return null;
return new Distribution
{
Id = source.Connection?.Id,
X = result.X,
Y = result.Y
};
}
}
Mapping Pofile
CreateMap<Source, Destination>()
.ForMember(d => d.Distribution, opt => opt.MapFrom( new CustomDistributorResolver()));
I always get Distribution value as NULL.
I am not sure what I am doing wrong here on mapping.
-Alan-
I am not very sure what you will deal with the IEnumerable. So I use linq to approach.
var id = Source.Connection.Id;
var distributions = Source.Connection.Distributors.Select(m=> new Distribution()
{
Id = id,
X = m.X,
Y = m.Y,
});
Automapper would like :
CreateMap<Source, Destination>()
.ForMember(dest => dest.Distribution.Id ,opt => opt.MapFrom(src => src.Connection.Id))
.ForMember(dest => dest.Distribution.X, opt => opt.MapFrom(src => src.Connection.Distributors.FirstOrDefault().X))
.ForMember(dest => dest.Distribution.Y, opt => opt.MapFrom(src => src.Connection.Distributors.FirstOrDefault().Y));
You can use a type converter
private class DestinationConverter : ITypeConverter<Source, Destination>
{
public Destination Convert(Source source,
Destination destination,
ResolutionContext context)
{
var result = source.Connection.Distributors.FirstOrDefault(x => x.Type == "ABC");
if (result == null) return null;
destination = new Destination();
destination.Distribution = new Distribution
{
Id = source.Connection?.Id,
X = result.X,
Y = result.Y
};
return destination;
}
}
and register the converter
CreateMap<Source, Destination>().ConvertUsing<DestinationConverter>();

Automapper configuration for grouping the data

I am having the following models
Source:
public class Opportunity
{
public Guid Id { get; set; }
public string Name { get; set; }
public Guid QuotationId { get; set; }
public int? QuotationNumber { get; set; }
public int? QuotationVersionNumber { get; set; }
}
Target:
public class OpportunityDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public List<QuotationDto> Quotations { get; set; }
}
public class QuotationDto
{
public Guid Id { get; set; }
public int Number { get; set; }
public int VersionNumber { get; set; }
}
The data I would fetch from my database would be flat as the Opportunity model and my api is exposing the OpportunityDto model.
so, in my auto-mapper configuration, I have the following code:
services
.AddSingleton(new MapperConfiguration(cfg =>
{
cfg.CreateMap<OpportunityDto, Opportunity>().ReverseMap();
cfg.CreateMap<QuotationDto, Quotation>().ReverseMap();
}).CreateMapper())
what I want to achive is a list of unique opportunities and each opportunity would have a nested member which would have the list of the quotations.
how can I make the automapper perform this grouping? right now the Quotations member of the opportunityDto returned from the api is always empty.
AutoMapper Configuration
You can do something like the following:
public static void InitialiseMapper()
{
Mapper.Initialize(cfg =>
{
cfg.CreateMap<IEnumerable<Opportunity>, OpportunityDto>()
.ForMember(x => x.Id, x => x.MapFrom(y => y.FirstOrDefault().Id))
.ForMember(x => x.Name, x => x.MapFrom(y => y.FirstOrDefault().Name))
.ForMember(x => x.Quotations,
x => x.MapFrom(y => Mapper.Map<IEnumerable<Opportunity>, IEnumerable<QuotationDto>>(y).ToArray()))
;
cfg.CreateMap<Opportunity, QuotationDto>()
.ForMember(x => x.Id, x => x.MapFrom(y => y.QuotationId))
.ForMember(x => x.Number, x => x.MapFrom(y => y.QuotationNumber))
.ForMember(x => x.VersionNumber, x => x.MapFrom(y => y.QuotationVersionNumber))
;
});
}
Test
Which then successfully knows how to map as demonstrated by the following test:
[TestMethod]
public void TestMethod1()
{
var oppo1Guid = Guid.NewGuid();
var opportunities = new List<Opportunity>
{
new Opportunity
{
Id = oppo1Guid,
Name = "Mikeys Oppurtunity",
QuotationId = Guid.NewGuid(),
QuotationNumber = 169,
QuotationVersionNumber = 80,
},
new Opportunity
{
Id = oppo1Guid,
Name = "Mikeys Oppurtunity",
QuotationId = Guid.NewGuid(),
QuotationNumber = 170,
QuotationVersionNumber = 20,
}
};
var dtos = Mapper.Map<IEnumerable<Opportunity>, OpportunityDto>(opportunities);
var json = JsonConvert.SerializeObject(dtos, Formatting.Indented);
Console.WriteLine(json);
}
And the output is
Test Output
{
"Id": "623c17df-f748-47a2-bc7e-35eb124dbfa3",
"Name": "Mikeys Oppurtunity",
"Quotations": [
{
"Id": "ad8b31c2-6157-4b7f-a1f2-9f8cfc1474b7",
"Number": 169,
"VersionNumber": 80
},
{
"Id": "515aa560-6a5b-47da-a214-255d1815e153",
"Number": 170,
"VersionNumber": 20
}
]
}
#Afflatus I would give you the idea that you can follow. I am assuming that you are using AspNetCore based on your services variable.
You can create an extension method like this, to later make a call on your ConfigurationServices like services.RegisterMappingsWithAutomapper():
public static IServiceCollection RegisterMappingsWithAutomapper(this IServiceCollection services)
{
var mapperConfig = AutoMapperHelper.InitializeAutoMapper();
services.AddScoped<IMapper>(provider => new Mapper(mapperConfig));
return services;
}
The InitializeAutoMapper is below:
public static class AutoMapperHelper
{
public static MapperConfiguration InitializeAutoMapper()
{
//Load here all your assemblies
var allClasses = AllClasses.FromLoadedAssemblies();
MapperConfiguration config = new MapperConfiguration(cfg =>
{
if (allClasses != null)
{
//here normally I add another Profiles that I use with reflection, marking my DTOs with an interface
cfg.AddProfile(new MappingModelsAndDtos(allClasses));
cfg.AddProfile(new MyCustomProfile());
}
});
return config;
}
}
now you need to implement the Profile, in this case MyCustomProfile
public class ModelProfile : Profile
{
public ModelProfile()
{
//put your source and destination here
CreateMap<MySource, MyDestination>()
.ConvertUsing<MySourceToMyDestination<MySource, MyDestination>>();
}
}
you need to implement MySourceToMyDestination class then.
Bellow is an example of the code of how I am using it in my projects
public class ApplicationModel2ApplicationDto : ITypeConverter<ApplicationModel, ApplicationDto>
{
public ApplicationDto Convert(ApplicationModel source, ApplicationDto destination, ResolutionContext context)
{
var mapper = context.Mapper;
try
{
destination = new ApplicationDto
{
ApplicationId = source.ApplicationId,
ApplicationName = source.ApplicationName,
Documents = mapper.Map<IEnumerable<DocumentDto>>(source.Documents),
Tags = mapper.Map<IEnumerable<TagInfoDto>>(source.TagInfos)
};
}
catch
{
return null;
}
return destination;
}
}
Hope this helps

Building up a where filter from a message based request

I have a small request object to filter by.
public class BufferFlatViewFilter
{
public bool? Active { get; set; }
public int? CustomerId { get; set; }
public int? TypeId { get; set; }
}
And I need to build up the where filter for the db request.
With Entity framework I was able to build up the Where Statement like so.
public List<BufferFlatView> GetBufferFlatView(BufferFlatViewFilter filter)
{
var data = db.qryCampaignOverView
.AsQueryable();
if (filter.TypeId.HasValue)
data = data.Where(x => x.TypeId == filter.TypeId.Value);
if (filter.CustomerId.HasValue)
data = data.Where(x => x.CustomerId == filter.CustomerId);
if (filter.Active)
data = data.Where(x => x.Active == filter.Active);
return data.ToList();
}
I'm not sure of how to build up the where statement for OrmLight given that Entity framework query is lazy loaded but not the OrmLight query.
We recently added expression chaining in the OrmLite ExpressionVisitor - code copy & pasted from the unit tests:
var visitor = dbConnection.CreateExpression<Person>();
visitor.Where(x => x.FirstName.StartsWith("Jim")).And(x => x.LastName.StartsWith("Hen"));
var results = db.Select<Person>(visitor);
Assert.AreEqual(1,results.Count);
visitor.Where(x => x.Age < 30).Or(x => x.Age > 45);
results = db.Select<Person>(visitor);
Assert.AreEqual(5, results.Count);
Assert.IsFalse(results.Any(x => x.FirstName == "Elvis"));
Note: Where(x => predicate) and .And(x => predicate) are functionally the same.
You can also build up your Order By expression
visitor.OrderBy(x => x.Name).ThenByDescending(x => x.Age);
So your code becomes
public List<BufferFlatView> GetBufferFlatView(BufferFlatViewFilter filter)
{
//assumes IDbConnection instance injected by IOC
var ev = dbConnection.CreateExpression<Campaign>();
if (filter.TypeId.HasValue)
ev.Where(x => x.TypeId == filter.TypeId.Value);
if (filter.CustomerId.HasValue)
ev.Where(x => x.CustomerId == filter.CustomerId);
if (filter.Active)
ev.Where(x => x.Active == filter.Active);
return dbConnection.Select<Campaign>(ev);
}
you could do something like this.
public class Poco
{
public int TypeId { get; set; }
public int CustomerId { get; set; }
public bool Active { get; set; }
}
public class Filter<T>
{
private List<Func<T, bool>> filters = new List<Func<T, bool>>();
public void AddFilter(Func<T, bool> filter)
{
this.filters.Add(filter);
}
public bool PredicateFilter(T item)
{
return filters.All(x => x(item));
}
}
static void Main(string[] args)
{
var list = new List<Poco>() { new Poco { Active = true, CustomerId = 1, TypeId = 1 } };
var filter = new Filter<Poco>();
filter.AddFilter(x => x.Active == false);
filter.AddFilter(x => x.CustomerId == 1);
filter.AddFilter(x => x.TypeId == 1);
var item = list.Where(x => filter.PredicateFilter(x));
Console.Read();
}

Categories