Mapping EF collection using AutoMapper and EF Reverse POCO Generator - asp.net-mvc

I was handed a project which is using EF Database first, EF Reverse POCO generator, and Auto Mapper. I'm really struggling getting a many-to-many relationship to be modeled properly using all three pieces and would appreciate any help.
I have the following tables in my DB:
+=================+ +==================+ +================+
| Event | | Event_Format | | Format |
+=================+ +==================+ +================+
| Id | | Id | | Id |
| Title | | EventId | | Name |
| Created | | FormatId | | Created |
| CreatedBy | | Created | | CreatedBy |
| | | CreatedBy | | |
+=================+ +==================+ +================+
This generates three POCO classes in my data layer:
public class Event {
public int Id { get; set; } // Id (Primary key)
public string Title { get; set; } // Title
public DateTime Created { get; set; } // Created
public string CreatedBy { get; set; } // CreatedBy
public virtual ICollection<EventFormat> EventFormats { get; set; } // Event_Format.FK_Event_Format_Event
}
public class EventFormat
{
public int Id { get; set; } // Id (Primary key)
public int EventId { get; set; } // EventId
public int FormatId { get; set; } // FormatId
public DateTime Created { get; set; } // Created
public string CreatedBy { get; set; } // CreatedBy
public virtual Event Event { get; set; } // FK_Event_Format_Event
public virtual Format Format { get; set; } // FK_Event_Format_Format
}
public class Format
{
public int Id { get; set; } // Id (Primary key)
public string Name { get; set; } // Name
public DateTime Created { get; set; } // Created
public string CreatedBy { get; set; } // CreatedBy
public virtual ICollection<EventFormat> EventFormats { get; set; } // Event_Format.FK_Event_Format_Format
}
In my MVC project I have a view model for Events and Formats (but not EventFormats as it seems unneeded).
public class Event.ViewModel
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime Created { get; set; }
public string CreatedBy { get; set; }
public List<Format.ViewModel> Formats { get; set; }
}
public class Format.ViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public String CreatedBy { get; set; }
}
Here are the Mapper profiles:
protected internal class BookingProfile : Profile
{
public new string ProfileName = "Admin_Booking";
protected override void Configure()
{
CreateMap<Data.Models.Event, Models.Booking.WisconsinFilmFest.ViewModel>()
.ForMember(dst => dst.Created, x => x.MapFrom(src => src.Created.ToLocalTime()))
.ForMember(dst => dst.EventFormats, x => x.MapFrom(src => src.EventFormats.ToList()));
}
}
protected internal class FormatProfile : Profile
{
public new string ProfileName = "Admin_Format";
protected override void Configure()
{
CreateMap<Data.Models.Format, Models.Format.ViewModel>()
.ForMember(dst => dst.Created, x => x.MapFrom(src => src.Created.ToLocalTime()))
.ForMember(dst => dst.Modified, x => x.MapFrom(src => src.Modified.ToLocalTime()));
}
}
When I attempt to use AutoMapper to map an Event to an Event.ViewModel it doesn't work because EF returns the EventFormat type instead (because of the extra columns). Is there a way to tell Automapper to get the Format of each EventFormat item and then map those into the Formats property on the ViewModel?
Currently what I'm doing is using the AfterMap() feature of Automapper, and looping through each item in EventFormats to get the Format and add it to the ViewModel's property like so:
protected internal class BookingProfile : Profile
{
public new string ProfileName = "Admin_Booking";
protected override void Configure()
{
CreateMap<Data.Models.Event, Models.Booking.WisconsinFilmFest.ViewModel>()
.ForMember(dst => dst.Created, x => x.MapFrom(src => src.Created.ToLocalTime()))
.ForMember(dst => dst.EventFormats, x => x.Ignore())
.AfterMap((src, dst) =>
{
if(src.EventFormats.Any(x => x.Format.Deleted == null))
{
foreach(Data.Models.EventFormat ef in src.EventFormats)
{
dst.EventFormats.Add(Mapper.Map(ef.Format, new Models.Format.ViewModel()));
}
}
});
}
This feels somewhat hacky to me and I was hoping there was a better way to do this.

Sure you can. Specify that AutoMapper should map Event.ViewModel.Formats to EventFormats.Select(ef => ef.Format):
Mapper.CreateMap<Format,Format.ViewModel>();
Mapper.CreateMap<Event,Event.ViewModel>()
.ForMember(dest => dest.Formats,
m => m.MapFrom(src => src.EventFormats.Select(ef => ef.Format)));
Now you can do
var result = db.Events.ProjectTo<Event.ViewModel>();

You should be able to just update the first mapping in the profile to read like this:
CreateMap<Data.Models.Event, Models.Booking.WisconsinFilmFest.ViewModel>()
.ForMember(dst => dst.Created, x => x.MapFrom(src => src.Created.ToLocalTime()))
.ForMember(dst => dst.Formats, x => x.MapFrom(src => src.EventFormats.Select(y => y.Format).ToList()));
As it stands, the profile is telling the mapper to project the source type's EventFormats property into the destination's EventFormats property, when it sounds like what you really want is to project the EventFormats.Format property into a Formats collection.

Related

ASP.NetCore OData DTO $expand navigation property results in empty array

I am using Entity Framework with OData to get data from my mysql database but I don't want to expose database entites to the user, so I've created some DTO's and map them with Automapper.
My Problem is that everything works fine except loading entities with $expand.
There are 2 Entities with 2 DTO's (in my project the dto's and domain models do not look the same, this is only for better reading):
public partial class Product
{
public string Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public virtual ICollection<ProductPrice> ProductPrices { get; set; }
}
public class ProductDTO
{
[Key]
public string Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public virtual ICollection<ProductPriceDTO> ProductPrices { get; set; }
public virtual CategoryDTO Category { get; set; }
}
public partial class Category
{
public int Id { get; set; }
public string Title { get; set; }
}
public class CategoryDTO
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
}
public partial class ProductPrice
{
public string VendorId { get; set; }
public string ProductId { get; set; }
public decimal Price { get; set; }
public virtual Product Product { get; set; }
public virtual Vendor Vendor { get; set; }
}
public class ProductPriceDTO
{
[Key]
[ForeignKey("Vendor")]
public string VendorId { get; set; }
[Key]
[ForeignKey("Product")]
public string ProductId { get; set; }
public decimal Price { get; set; }
public virtual VendorDTO Vendor { get; set; }
public virtual ProductDTO Product { get; set; }
}
The models are created the following way:
public IEdmModel GetEdmModel(IServiceProvider serviceProvider)
{
var builder = new ODataConventionModelBuilder(serviceProvider);
builder.Namespace = "Functions";
//category
builder.EntitySet<CategoryDTO>("Categories").EntityType.Select().Filter().OrderBy().Expand().Count().Page();
//product
builder.EntitySet<ProductDTO>("Products").EntityType.Select().Filter().OrderBy().Expand().Count().Page();
return builder.GetEdmModel();
//productprice
builder.EntitySet<ProductPriceDTO>("ProductPrices").EntityType.Select().Filter().OrderBy().Expand().Count().Page();
}
Automapper profile:
public AutoMapperProfile()
{
CreateMap<Product, ProductDTO>()
.ForMember(dto => dto.Category, conf => conf.AllowNull())
.ForMember(dto => dto.ProductPrices, dest => dest.MapFrom(x => x.ProductPrices))
.ForMember(dto => dto.ProductPrices, dest => dest.ExplicitExpansion())
.ForMember(dto => dto.ProductPrices, conf => conf.AllowNull());
CreateMap<ProductPrice, ProductPriceDTO>()
.ForMember(dto => dto.Product, conf => conf.AllowNull())
.ForMember(dto => dto.Vendor, conf => conf.AllowNull());
}
Controller:
[Authorize]
[ODataRoutePrefix("Products")]
public class ProductsController : BaseODataController
{
private readonly IProductService ProductService;
private readonly IProductPriceService ProductPriceService;
public ProductsController(IMapper mapper, IProductService productService, IProductPriceService productPriceService) : base(mapper)
{
ProductService = productService;
ProductPriceService = productPriceService;
}
[AllowAnonymous]
[EnableQuery]
public IQueryable<ProductDTO> Get(ODataQueryOptions queryOptions)
{
var query = ProductService.QueryProducts();
string[] includes = GetExpandNamesFromODataQuery(queryOptions);
if (includes != null && includes.Length > 0)
{
return query.ProjectTo<ProductDTO>(null, includes);
}
return query.ProjectTo<ProductDTO>();
}
[AllowAnonymous]
[EnableQuery]
[ODataRoute("({key})")]
public IQueryable<ProductDTO> Get([FromODataUri] string key, ODataQueryOptions queryOptions)
{
var query = ProductService.QueryProducts().Where(x => x.Id.Equals(key));
string[] includes = GetExpandNamesFromODataQuery(queryOptions);
if (includes != null && includes.Length > 0)
{
return query.ProjectTo<ProductDTO>(null, includes);
}
return query.ProjectTo<ProductDTO>();
}
}
As I mentioned above every query works fine ($select, $filter, $orderBy, $count).
But when I call the following:
https://localhost:44376/odata/Products('631794')?$expand=Category
I get:
{"#odata.context":"https://localhost:44376/odata/$metadata#Products","value":[
as response.
In the output of Visual Studio there is a message:
No coercion operator is defined between types 'System.Int16' and 'System.Boolean'.
I think there must be something wrong with the Automapper profile. As I read somewhere .ProjectTo() with include parameters creates a Select to get the related data from the navigation property. I thought it is enough to create the relation with [ForeignKey] in the DTO.

Retrieving associated elements based on ID in entity query

I have an entity query which groups the number of property reviews to a property. However im having some trouble with the query to pull the rest of the data on the property based on its propertyID. Thanks!
Example Results:
|PropertyID | NumOfReviews | StreetAddress | City |
1 14 1600 Speaker St. Miami
Query:
var query1 = from r in db.Reviews
group r by r.propertyID into g
select new
{
propertyID = g.Key,
numofReviews = g.Count()
//Get Rest of data
};
Property Model:
public partial class Property
{
public int propertyID { get; set; }
public string streetaddress { get; set; }
public string city { get; set; }
public string zip { get; set; }
public string state { get; set; }
public string country { get; set; }
public string route { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
}
Review Model:
public partial class Review
{
public int reviewID { get; set; }
public int propertyID { get; set; }
public int rating { get; set; }
public string review { get; set; }
public virtual Property Property { get; set; }
}
Can you come at it from the opposite direction?
var query = from p in db.Properties
select new {
propertyId = p.PropertyId,
numofReviews = p.Reviews.Count()
//Grab remaining properties off of the p variable that you need
};

How to GroupBy Virtual Column

Hello I have problems Table for Reeves and I am trying to check which reeve creates most problem. So here is my Problem Model;
public int Id { get; set; }
public string Name { get; set; }
public string Text { get; set; }
public short Sira { get; set; }
public DateTime Created { get; set; }
public virtual Topic Topic { get; set; }
public virtual Member WhoIs { get; set; }
public virtual Province Province { get; set; }
public virtual Town Town { get; set; }
public virtual District District { get; set; }
public virtual Reeve Reeve { get; set; }
As you see I have Reeve as a virtual table.When I create table I get Reeve_Id and Trying to .GroupBy(m => m.Reeve.Id) So here is the code ;
public ActionResult GroupByReeve()
{
var model = new ProblemModel
{
ProblemsList = Db.Problems.GroupBy(m => m.Reeve.Id)
.Select(s => new ProblemModel.ProblemForView
{
Id = s.FirstOrDefault().Id,
CountItem = s.Count(),
AllItem = Db.Problems.Count(),
ReeveName=s.FirstOrDefault().Reeve.Name
})
};
return View(model);
}
So I can groupby problems by their Id not by ReeveId.The view returns 4 items(4 problems) But they are belong to only 2 Reeves So it must show 2 items not 4 items. How can I do it ?
I'm not sure, why your code doesn't work (it looks like it should), but here is how you use GroupBy properly:
ProblemsList = Db.Problems.GroupBy(m => m.Reeve.Id,
s => new ProblemModel.ProblemForView
{
Id = s.FirstOrDefault().Id,
CountItem = s.Count(),
AllItem = Db.Problems.Count(),
ReeveName=s.FirstOrDefault().Reeve.Name
});

ef one-to-one referential integrity constraint violation

There are three entities:
class A
{
public int Id { get; set; }
public string Z { get; set; }
public virtual B Bc { get; set; }
public virtual C Cc { get; set; }
}
class B
{
public int Id { get; set; }
public string X { get; set; }
public virtual A Ac { get; set; }
}
class C
{
public int Id { get; set; }
public string Y { get; set; }
public virtual A Ac { get; set; }
}
I would like the 'A' class to be in a one-to-one connection with the 'B' class or 'C' class but only with one of them.
The one-to-one connections are ready among tables in the database.
I tried to use fluent mapping to establish their relationship but I always got error when I wanted to insert A:
A referential integrity constraint violation occurred: The property value(s) of 'A.ID' on one end of a relationship do not match the property value(s) of 'B.ID' on the other end.
modelBuilder.Entity<A>()
.HasOptional(x => x.B)
.WithRequired();
modelBuilder.Entity<A>()
.HasOptional(x => x.C)
.WithRequired();
OR
modelBuilder.Entity<B>()
.HasRequired(t => t.A)
.WithOptional(t => t.B);
modelBuilder.Entity<C>()
.HasRequired(t => t.A)
.WithOptional(t => t.C);
What is the solution, please?
EDIT:
I save A.
B.ID = A.ID
I save B.

Entity framework to object with collection, with Automapper, exception on update

I have an EF 4.1 model, two tables are generated PERSON and ADDRESS from my database.
//This method works
public void Update(IPerson person)
{
var personDb = _dataContext.PERSON.SingleOrDefault(x => x.ID == person.Id);
Mapper.Map<Person, PERSON>((Person)person, personDb);
_dataContext.SaveChanges();
}
But when I remove the .Ignore() in Automapper mapping, I get this exception :
The EntityCollection could not be initialized because the relationship manager for the object to which the EntityCollection belongs is already attached to an ObjectContext. The InitializeRelatedCollection method should only be called to initialize a new EntityCollection during deserialization of an object graph.
I'd like when I added an new address to the existing addresses save the person and address.
Any idea ?
Thanks,
public void AutomapperInit()
{
Mapper.CreateMap<Person, PERSON>()
.ForMember(x => x.ADDRESS, opt => opt.Ignore());
Mapper.CreateMap<PERSON, Person>()
.ForMember(dest => dest.Address, option => option.MapFrom(src => src.ADDRESS.Select(address => Mapper.Map<ADDRESS, Address>(address)).ToList()));
Mapper.CreateMap<Address, ADDRESS>();
Mapper.CreateMap<ADDRESS, Address>()
.ForMember(dest => dest.Rue, option => option.MapFrom(src => src.STREET));
}
public interface IPerson
{
int Id { get; set; }
string FirstName { get; set; }
string LastName { get; set; }
ICollection<IAddress> Address { get; set; }
}
public interface IAddress
{
string Rue { get; set; }
string Number { get; set; }
int PersonId { get; set; }
}
class Person : IPerson
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<IAddress> Address { get; set; }
}
class Address : IAddress
{
public string Rue { get; set; }
public string Number { get; set; }
public int PersonId { get; set; }
}
Does
var personDb = Mapper.Map<Person, PERSON>((Person)person, personDb);
_dataContext.Attach(personDb);
_dataContext.SaveChanges();
give the results you expect, or am I misinterpreting your question?

Resources