Using AutoMapper with EF Code First joins whilst sharing mappings - c#

Given the following:
public class Foo
{
public Int32 Foo_PK { get; set; }
public String SomeProperty { get; set; }
}
public class Bar
{
public Int32 Bar_PK { get; set; }
public Int32 Foo_FK { get; set; }
public String SomeOtherProperty { get; set; }
}
public class JoinResult<TEntity, TJoiningEntity>
{
public TEntity From { get; private set; }
public TEntity To { get; private set; }
public JoinResult(TEntity from, TEntity to)
{
this.From = from;
this.To = to;
}
}
public interface IFooResult
{
public String SomeProperty { get; set; }
}
public interface IBarResult : IFooResult
{
public String SomeOtherProperty { get; set; }
}
public class FooResultDTO : IFooResult, IBarResult
{
public String SomeProperty { get; set; }
public String SomeOtherProperty { get; set; }
}
The idea behind this is that we some method of dispensing foo's and foo's with other related records, e.g. if there are 4 bar's then 4 rows in a table with the additional fields.
public class FooDispensary
{
public IQueryable<T> Dispense<T>()
where T: IFooResult
{
using (var repository = new Repository())
{
// TODO: Handle mapping for Foo -> FooResult
// Project to
return repository.Foos.ProjectTo<FooResultDTO>();
}
}
public IQueryable<T> DispenseWithBars<T>()
where T : IFooResult, IBarResult
{
using (var repository = new Repository())
{
// TODO: Handle mapping for JoinResult.From (same as Foo -> FooResult) as well as to JoinResult.To
// Project to
return repository.Foos.Join((f) => f.Foo_PK,
(b) => b.Foo_FK,
(f, b) => new JoinResult<Foo, Bar>(f, b))
.ProjectTo<FooResultDTO>();
}
}
}
However, I would ideally like to only specify the base mapping once (Foo -> IFooResult) and then re-use this in the methods where we need to join to a child table.
There are multiple reasons behind wanting to do this which are specific to my project however no need to go into them, I am just wondering if this is possible as I have struggled with the syntax thus far?
Thanks

Create a Map between Foo and FooResult. Because the Property SomeProperty is named the same in both the source and target Automapper will be able to figure out the mapping implicitly.
// TODO: Handle mapping for Foo -> FooResult
AutoMapper.Mapper.CreateMap<Foo, FooResult>();
Then create a map between JoinResult<Foo, Bar> and FooResultDTO
// TODO: Handle mapping for JoinResult.From (same as Foo -> FooResult) as well as to JoinResult.To
AutoMapper.Mapper.CreateMap<JoinResult<Foo, Bar>, FooResultDTO>()
.ForMember(r => r.SomeProperty, opt => opt.MapFrom(f => f.From.SomeProperty)
.ForMember(r => r.SomeOtherProperty, opt => opt.MapFrom(f => f.To.SomeOtherProperty)
However, I would ideally like to only specify the base mapping once (Foo -> IFooResult) and then re-use this in the methods where we need to join to a child table.
You're not resusing the mapping between Foo and IFooResult anywhere in your example. Your second function needs to map between JoinResult<Foo, Bar> and FooResultDTO as shown above. If you need to reuse mappings I suggest you look into using an AutoMapper Profile and managing a singleton AutoMapper instance that can be shared between your functions: https://github.com/AutoMapper/AutoMapper/wiki/Configuration

Related

AutoMapper - Get error when trying to map two classes

I am trying to use AutoMapper to map a DTO to an Entity class but I keep getting an error.
Here is the DTO Class:
public class Product
{
public string ID { get; set; }
public string SKU { get; set; }
public string Name { get; set; }
public PriceTiers PriceTiers { get; set; }
}
and here is the Entity:
public partial class Product
{
public Product()
{
PriceTiers = new List<PriceTiers>();
}
[Key]
public string ID { get; set; }
public string SKU { get; set; }
public string Name { get; set; }
public virtual ICollection<PriceTiers> PriceTiers { get; set; }
}
Why do I keep getting the following error?
{"Missing type map configuration or unsupported
mapping.\r\n\r\nMapping types:\r\nPriceTiers ->
ICollection1\r\nWeb.Areas.DEAR.DTOs.PriceTiers -> System.Collections.Generic.ICollection1[[Web.Areas.DEAR.Data.PriceTiers,
Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]\r\n\r\n
Destination Member:\r\nPriceTiers\r\n"}
This is what I have in the Profile class:
AllowNullCollections = true;
CreateMap<DTOs.Product, Data.Product>();
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
and this is what I use to map the classes:
var products = _mapper.Map<IEnumerable<Product>>(result.Products);
This is what is in the Program.cs:
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
The exception message is quite clear, the AutoMapper doesn't know how to map the data from DTOs.PriceTiers to ICollection<Data.PriceTiers>.
Solution 1: Map from DTOs.PriceTiers to ICollection<Data.PriceTiers>
I believe that Custom Type Converters is what you need.
Create Custom Type Converters.
public class ICollectionDataPriceTiersTypeConverter : ITypeConverter<DTOs.PriceTiers, ICollection<Data.PriceTiers>>
{
public ICollection<Data.PriceTiers> Convert(DTOs.PriceTiers src, ICollection<Data.PriceTiers> dest, ResolutionContext context)
{
if (src == null)
return default;
var singleDest = context.Mapper.Map<Data.PriceTiers>(src);
return new List<Data.PriceTiers>
{
singleDest
};
}
}
Add to mapping profile.
CreateMap<DTOs.PriceTiers, ICollection<Data.PriceTiers>>()
.ConvertUsing<ICollectionDataPriceTiersTypeConverter>();
Demo # .NET Fiddle
Solution 2: Map from ICollection<DTOs.PriceTiers> to ICollection<Data.PriceTiers>
If the PriceTiers in DTOs.Product supports multiple items and mapping with many to many (to ICollection<Data.ProductTiers>), then consider modifying the property as the ICollection<DTOs.PriceTiers> type.
namespace DTOs
{
public class Product
{
...
public ICollection<PriceTiers> PriceTiers { get; set; }
}
}
Did you added "CreateMapper()" method after your configurations?
Try something like that.
public class MappingProfile : Profile
{
public MappingProfile {
AllowNullCollections = true;
CreateMap<DTOs.Product, Data.Product>();
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
}
}
After that, on your container service, inject this dependency:
var mappingConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
After some more research I found out that my mapping profile was not in the right order. These are the changes I made.
public class AutoMapperProfiles : Profile
{
public AutoMapperProfiles()
{
AllowNullCollections = true;
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
CreateMap<DTOs.Product, Data.Product>()
.ForMember(dto => dto.PriceTiers, opt => opt.MapFrom(x => x.PriceTiers));
}
}
Now it maps perfectly

Strange Automapper behavior for derived object mapping

guys.
I have a strange behavior of Automapper when I try to map derived objects from one root.
For example:
public class User
{
public Guid Id { get; set; }
}
public class DerivedUser1 : User
{
public int SomeProp1 { get; set; }
}
public class DerivedUser2 : User
{
public int SomeProp2 { get; set; }
public class SubDerivedUser3 : DerivedUser2
{
public int SomeProp3 { get; set; }
}
I use CreateMap like this:
CreateMap<User, User>()
.ForMember(e => e.Id, f => f.Ignore())
.IncludeAllDerived()
.AfterMap((src, dest) =>
{
dest.Id = Guid.NewGuid();
});
Let see the code of mapping below:
var testUser1 = new DerivedUser1();
var testUser2 = new SubDerivedUser3();
mapper.Map(testUser2, testUser1, testUser2.GetType(), testUser1.GetType());
When I try to execute this code, I have an error:
"No coercion operator is defined between types 'DerivedUser1' and
'DerivedUser2'"
I thought that IncludeAllDerived() always check a whole derivation tree. Could someone explain to me the reason of this error?
Thank you.

Using a single Entity Framework Core DbContext to manage multiple database schemas with homonymous tables

In a .NET Core 2.1 library I need to access to a MySQL database organized in multiple schemas with tables that can have the same name across those schemas. I can't make any changes to the DB since it comes from another company.
For most of the tables I need a read-only access and I'd like to use a single EF Core DbContext.
Actually I get this error message during initialization:
InvalidOperationException: Cannot use table 'tbl_panel' for
entity type 'Db2Panels' since it is being used for entity
type 'Db1Panels' and there is no relationship between their
primary keys.
I think that the crux of the matter mainly resides in the configuration methods, which should be called not just once but N times, one for each instance of the entity with different schema (db_machine_1.tbl_panel, db_machine_2.tbl_panel, etc.).
How can I reach my goal?
This is my actual implementation.
Database schemas
// db_machine_1 schema
db_machine_1.tbl_panel
db_machine_1.tbl_basket
db_machine_1.tbl_unit
// db_machine_2 schema
db_machine_2.tbl_panel
db_machine_2.tbl_basket
db_machine_2.tbl_discard
// Other db_machine_X schemas with similar structure...
DbContext configuration
public class MyDbContext : DbContext
{
// Schema: db_machine_1
public DbSet<Panel> Db1Panels { get; set; }
public DbSet<Basket> Db1Baskets { get; set; }
public DbSet<Unit> Db1Units { get; set; }
// Schema: db_machine_2
public DbSet<Panel> Db2Panels { get; set; }
public DbSet<Basket> Db2Baskets { get; set; }
public DbSet<Discard> Db2Discards { get; set; }
// Other schemas DbSet<X> objects...
// Arrays to access the specific DbSet by using the schema number:
// Panels[1] -> Db1Panels, Panels[2] -> Db2Panels, ...
public DbSet<Panel>[] Panels { get; }
public DbSet<Basket>[] Baskets { get; }
// Other arrays for other DbSet<X> objects...
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options)
{
// Arrays initialization
List<DbSet<Panel>> dbPanelList = new List<DbSet<Panel>>();
dbPanelList.Add(Db1Panels);
dbPanelList.Add(Db2Panels);
Panels = dbPanelList.ToArray();
List<DbSet<Basket>> dbBasketList = new List<DbSet<Basket>>();
dbBasketList.Add(Db1Baskets);
dbBasketList.Add(Db2Baskets);
Baskets = dbBasketList.ToArray();
// Initialization for other DbSet<X> objects...
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyAllConfigurations<MyDbContext>();
modelBuilder.ApplyAllConversions();
}
}
Objects
public class Panel
{
public long Id { get; set; }
public string SN { get; set; }
// Other properties...
}
public class Basket
{
public long Id { get; set; }
public string Description { get; set; }
// Other properties...
}
Configurations
public class PanelConfiguration : IEntityTypeConfiguration<Panel>
{
public void Configure(EntityTypeBuilder<Panel> builder)
{
builder.ToTable("tbl_panel");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id)
.HasColumnName("ID_Record");
builder.Property(e => e.SN)
.HasColumnName("Serial")
.HasMaxLength(20);
// Other properties configuration...
}
}
public class BasketConfiguration : IEntityTypeConfiguration<Basket>
{
public void Configure(EntityTypeBuilder<Basket> builder)
{
builder.ToTable("tbl_basket");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id)
.HasColumnName("ID_Record");
builder.Property(e => e.Description)
.HasColumnName("Desc")
.HasMaxLength(100);
// Other properties configuration...
}
}
// Other IEntityTypeConfiguration implementations for other tables...
// This extension method is used to automatically load all Configurations
// of the various entities
public static class ModelBuilderExtensions
{
public static void ApplyAllConfigurations(this ModelBuilder modelBuilder)
{
var applyConfigurationMethodInfo = modelBuilder
.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.First(m => m.Name.Equals("ApplyConfiguration", StringComparison.OrdinalIgnoreCase));
var ret = typeof(T).Assembly
.GetTypes()
.Select(t => (t, i: t.GetInterfaces().FirstOrDefault(i => i.Name.Equals(typeof(IEntityTypeConfiguration<>).Name, StringComparison.Ordinal))))
.Where(it => it.i != null)
.Select(it => (et: it.i.GetGenericArguments()[0], cfgObj: Activator.CreateInstance(it.t)))
.Select(it => applyConfigurationMethodInfo.MakeGenericMethod(it.et).Invoke(modelBuilder, new[] { it.cfgObj }))
.ToList();
}
}
UPDATE about base class arrays
After creating base abstract classes and derived ones, I'd like to merge all the derived class objects into a single array to be able to access the specific DbSet by using the schema number. See also above code of DbContext constructor.
I'm having problems with casting...
List<DbSet<Panel>> dbPanelList = new List<DbSet<Panel>>();
dbPanelList.Add((DbSet<Panel>)Db1Panels.Select(g => g as Panel)); // NOT WORKING! Cast Exception
dbPanelList.Add((DbSet<Panel>)Db2Panels.Cast<DbSet<Panel>>()); // NOT WORKING! Cast Exception
Panels = dbPanelList.ToArray();
Is this possible somehow?
I think you can't get away from having two different EF objects for the different tables, and you probably shouldn't as they may diverge at some point in the future.
At a minimum you need two classes Db1Panel and Db2Panel . I assume that actually the "Db" prefix is meant to meant a different schema, not actually a different database.
However that shouldn't be a big problem as there are other ways within C# of making them behave in similar fashions. Two options that spring to mind are having them inherit from the same base class, or have them implement an interface:
public abstract class PanelBase
{
public long Id { get; set; }
// other properties
}
[Table("tbl_panel", Schema = "Db1")]
public class Db1Panel : PanelBase{}
[Table("tbl_panel", Schema = "Db2")]
public class Db2Panel : PanelBase{}
If you chose to implement the interface you would need to repeat the properties in each class, but refactoring tools make this quite easy.
public interface IPanel
{
public long Id { get; set; }
}
[Table("tbl_panel", Schema = "Db1")]
public class Db1Panel : IPanel
{
public long Id { get; set; }
}
[Table("tbl_panel", Schema = "Db2")]
public class Db2Panel : IPanel
{
public long Id { get; set; }
}
Or depending on the size of your application you could consider having another namespace of domain objects and just map the database objects into it:
You should be able to use the Table attribute. There's a parameter Schema that allows you to set the schema name. See here for documentation. In your case you'd get something like
[Table("Table1", Schema="Schema1")]
public class Entity1Schema1
{
public string Property1 {get;set;}
}
[Table("Table1", Schema="Schema2")]
public class Entity1Schema2
{
public string Property1 {get;set;}
}
And then of course you can use interfaces or base classes to refactor your code as #ste-fu already mentioned.

Config Automapper to ignore type when it's an inner-inner property but not inner property

This one takes a little explaining. I have a set of types such that;
public class Child
{
public int ID { get; set;}
}
public class MayHaveChild
{
public Child Value { get; set; }
public int MayID { get; set; }
}
public class MustNotHaveChild { get; set; }
{
public List<MayHaveChild> MayValues { get; set; }
}
In the above scenario, I want any mapping of MayHaveChild to have the values for the Child object, except when I have mapped MustNotHaveChild. E.g.;
When I have
//...some code
MayHave obj = Mapper.Map<MayHaveChild>(childObj);
// I want to be able to access obj.Child.ID
But when I have
//...some code
MustNotHave obj = Mapper.Map<MustNotHaveChild>(notHaveObj);
// I want to be able to access obj.MayValues[0].MayID but
// *not* obj.MayValues[0].Value
I've been through the automapper documention on nesting, polymorphism, lists, etc and I can't find anything that quite matches what I want.
I could solve this by having a inheriting the MayHave class to a MustNotHave variant but this would involve changing quite a lot of existing code. Is there a way to configure Automapper in the manner I need?
I couldn't find a way to configure AutoMapper the way I wanted without going down the inheritance route - though this proved less problematic than I thought. I did something like the following;
public class NoChild : MayHaveChild
{
}
public class MustNotHaveChild { get; set; }
{
// \/--datatype change here
public List<NoChild> MayValues { get; set; }
}
Then, later in the AutoMapper config;
Mapper.CreateMap<MayHave, NoChild>()
.ForMember(c => c.Child, opt => opt.Ignore());

How to work with multiple DbSets with common base class in EF 5?

I have 2 POCO classes:
class Email: Base
{
public int SomeProperty { get; set; }
}
class Photo: Base
{
public int SomeOtherProperty { get; set; }
}
and a base class
abstract class Base
{
public DateTime DateCreated { get; set; }
}
here is my context definition:
public class EntitiesContext : DbContext
{
public DbSet<Email> Emails { get; set; }
public DbSet<Photo> Photos { get; set; }
}
of course these classes here are just for the sake of example, things are quite more complicated.
base class is only intended to have common properties for each table - date modified, state, etc. I believe I use Table-Per-Type approach.
PROBLEM: I have some common business logic which I need to run against each table (for example, count non-processed items of each type). I need a way to iterate through a set of tables with common base class. I was hoping to do something like this:
private void GoThroughAllTables(Action<DbSet<Base>> fnProcess, bool needSave)
{
using (var db = new EntitiesContext())
{
fnProcess(db.Emails);
fnProcess(db.Photos);
if (needSave == true)
{
db.SaveChanges();
}
}
}
public IEnumerable<QueueStatus> GetQueueStatus()
{
var res = new List<QueueStatus>();
GoThroughAllTables((set) =>
{
res.Add(new QueueStatus
{
Count = set.Cast<Base>().Count(x => x.DateCreated > someDate),
});
}, false);
return res;
}
public void DeleteFailedItems()
{
GoThroughAllTables((set) =>
{
set.Cast<Base>().Remove(x => x.DateCreated > someDate);
}, true);
return res;
}
this will not compile:
fnProcess(db.Emails);
Argument 1: cannot convert from 'System.Data.Entity.DbSet|Email|' to
'System.Data.Entity.DbSet|Base|'
passing non-typed DbSet will not work because Cast will fail.
so I'm not sure what else can I try. Any suggestions?

Categories