I have the following definition for a Transaction (as in purchase details) object :
public class Transaction : MappingObject
{
public virtual int Id { get; set; }
public virtual IList<TransactionProduct> Products { get; set; }
}
public class TransactionMap : ClassMapping<Transaction>
{
public TransactionMap()
{
Table("TRANSACTIONS_TBL");
Id(x => x.Id, m =>
{
m.Column("ID");
m.Generator(Generators.Identity);
});
Bag(x => x.Products, m =>
{
m.Inverse(true);
m.Table("TRANSACTION_PRODUCTS_TBL");
m.Key(k => k.Column("TRANSACTION_ID"));
m.Lazy(CollectionLazy.NoLazy);
},
relation => relation.OneToMany(mapper => mapper.Class(typeof(TransactionProduct))));
}
}
And TransactionProduct is defined like this :
public class TransactionProduct : MappingObject
{
public virtual int TransactionId { get; set; }
public virtual int ProductId { get; set; }
public virtual int Quantity { get; set; }
public override bool Equals(object obj)
{
var t = obj as TransactionProduct;
if (t == null)
return false;
if (TransactionId == t.TransactionId && ProductId == t.ProductId)
return true;
return false;
}
public override int GetHashCode()
{
return (TransactionId + "|" + ProductId).GetHashCode();
}
}
public class TransactionProductMap : ClassMapping<TransactionProduct>
{
public TransactionProductMap()
{
Table("TRANSACTION_PRODUCTS_TBL");
ComposedId(map =>
{
map.Property(x => x.TransactionId, m => m.Column("TRANSACTION_ID"));
map.Property(x => x.ProductId, m => m.Column("PRODUCT_ID"));
});
Property(x => x.Quantity, m => m.Column("QUANTITY"));
}
}
Now, I want to select a transaction and populate the Products array in a single select (I know I can select the transaction then the products but It's bad practice)
So I'm using this :
using (var session = CommonDAL.GetSession())
{
Transaction transactionAlias = null;
TransactionProduct transactionProductAlias = null;
return session.QueryOver(() => transactionAlias).
JoinAlias(() => transactionAlias.Products, () => transactionProductAlias).
Where(() => transactionAlias.Id == transactionProductAlias.TransactionId).List().ToList();
}
This work's quite well but the problem is that if I have a transaction with 2 products, I get 2 transaction objects with 2 products inside them, same goes for if I have a transaction with 4 products, I get 4 transaction objects with 4 products. The transaction objects are good, but the problem is the duplicates.
I can probably solve it with Distinct() but again, I want best practice
I solved it using .TransformUsing(Transformers.DistinctRootEntity) after the Where(...)
Related
My setup is that I have a basket which contains items. An item is made up of a product and a size. Products have a many to many relationship with sizes so that I can verify that a given size is valid for a given product. I would like to be able to add an item to the basket, perform some validation and save to the database.
I have created a demo program to demonstrate the problem I am having. When the program runs there is already a basket saved to the database (see the DBInitializer). It has one item which is a large foo. In the program you can see that I load the basket, load a small size and a bar product. I add the large bar to the basket. The basket does some internal validation and I save to the database. This works without error.
The problem comes when I try to add a product that already exists in the database with a different size. Hence if we try to add a large bar to the basket and save we get a null reference exception. This is not the behaviour I would like because a basket which contains 2 items, a large foo and a small foo, is perfectly valid.
I'm pretty sure the problem is to do with the fact that we have already loaded foo in the basket through eager loading. I've tried commenting out the eager loading for the basketitems and this works. However if possible I would like a solution which keeps the eager loading.
Notes: I have added an extra method to my dbcontext class which is int SaveChanges(bool excludeReferenceData). This stops extra product and size records being saved back to the database. I've made all my constructors, getters and setters public to make it easier to replicate my problem. My demo code was created on a console app targeting .net framework 4.5.2. The version of Entity framework is 6.2.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using static Demo.Constants;
namespace Demo
{
public static class Constants
{
public static int BasketId => 1;
public static int SmallId => 1;
public static int LargeId => 2;
public static int FooId => 1;
public static int BarId => 2;
}
public class Program
{
public static void Main()
{
using (var context = new AppContext())
{
var customerBasket = context.Baskets
.Include(b => b.Items.Select(cbi => cbi.Product))
.Include(b => b.Items.Select(cbi => cbi.Size))
.SingleOrDefault(b => b.Id == BasketId);
var size = context.Sizes.AsNoTracking()
.SingleOrDefault(s => s.Id == SmallId);
context.Configuration.ProxyCreationEnabled = false;
var product = context
.Products
.AsNoTracking()
.Include(p => p.Sizes)
.SingleOrDefault(p => p.Id == BarId);
//changing BarId to FooId in the above line results in
//null reference exception when savechanges is called.
customerBasket.AddItem(product, size);
context.SaveChanges(excludeReferenceData: true);
}
Console.ReadLine();
}
}
public class Basket
{
public int Id { get; set; }
public virtual ICollection<Item> Items { get; set; }
public Basket()
{
Items = new Collection<Item>();
}
public void AddItem(Product product, Size size)
{
if (itemAlreadyExists(product, size))
{
throw new InvalidOperationException("item already in basket");
}
var newBasketItem = Item.Create(
this,
product,
size);
Items.Add(newBasketItem);
}
private bool itemAlreadyExists(Product product, Size size)
{
return Items.Any(a => a.ProductId == product.Id && a.SizeId == size.Id);
}
}
public class Item
{
public Guid Id { get; set; }
public int BasketId { get; set; }
public virtual Product Product { get; set; }
public int ProductId { get; set; }
public virtual Size Size { get; set; }
public int SizeId { get; set; }
public Item()
{
}
public string getDescription()
{
return $"{Product.Name} - {Size.Name}";
}
internal static Item Create(Basket basket
, Product product,
Size size)
{
Guid id = Guid.NewGuid();
if (!product.HasSize(size))
{
throw new InvalidOperationException("product does not come in size");
}
var basketItem = new Item
{
Id = id,
BasketId = basket.Id,
Product = product,
ProductId = product.Id,
Size = size,
SizeId = size.Id
};
return basketItem;
}
}
public class Product : IReferenceObject
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ProductSize> Sizes { get; set; }
public Product()
{
Sizes = new Collection<ProductSize>();
}
public bool HasSize(Size size)
{
return Sizes.Any(s => s.SizeId == size.Id);
}
}
public class ProductSize : IReferenceObject
{
public int SizeId { get; set; }
public virtual Size Size { get; set; }
public int ProductId { get; set; }
}
public class Size : IReferenceObject
{
public int Id { get; set; }
public string Name { get; set; }
}
public class AppContext : DbContext
{
public DbSet<Basket> Baskets { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Size> Sizes { get; set; }
public AppContext()
: base("name=DefaultConnection")
{
Database.SetInitializer(new DBInitializer());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Basket>()
.HasMany(c => c.Items)
.WithRequired()
.HasForeignKey(c => c.BasketId)
.WillCascadeOnDelete(true);
modelBuilder.Entity<Item>()
.Property(c => c.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<Item>()
.HasKey(c => new { c.Id, c.BasketId });
modelBuilder.Entity<ProductSize>()
.HasKey(c => new { c.ProductId, c.SizeId });
base.OnModelCreating(modelBuilder);
}
public int SaveChanges(bool excludeReferenceData)
{
if(excludeReferenceData)
{
var referenceEntries =
ChangeTracker.Entries<IReferenceObject>()
.Where(e => e.State != EntityState.Unchanged
&& e.State != EntityState.Detached);
foreach (var entry in referenceEntries)
{
entry.State = EntityState.Detached;
}
}
return SaveChanges();
}
}
public interface IReferenceObject
{
}
public class DBInitializer: DropCreateDatabaseAlways<AppContext>
{
protected override void Seed(AppContext context)
{
context.Sizes.Add(new Size { Id = LargeId, Name = "Large" });
context.Sizes.Add(new Size { Id = SmallId, Name = "Small" });
context.Products.Add(
new Product
{
Id = FooId,
Name = "Foo",
Sizes = new Collection<ProductSize>()
{
new ProductSize{ProductId = FooId, SizeId = LargeId},
new ProductSize{ProductId = FooId, SizeId =SmallId}
}
});
context.Products.Add(new Product { Id = BarId, Name = "Bar",
Sizes = new Collection<ProductSize>()
{
new ProductSize{ProductId = BarId, SizeId = SmallId}
}
});
context.Baskets.Add(new Basket
{
Id = BasketId,
Items = new Collection<Item>()
{
new Item
{
Id = Guid.NewGuid(),
BasketId =BasketId,
ProductId = FooId,
SizeId = LargeId
}
}
});
base.Seed(context);
}
}
}
When you use AsNoTracking, this tells EF to not include the objects being loaded into the DbContext ChangeTracker. You normally want to do this when you load data to be returned and know you aren't going to want to save it back at that point. Thus, I think you just need to get rid of AsNoTracking on all of your calls and it should work fine.
I have three classes Product, Stock, StockId. Stock has a composite Id of Token and InternalCode, these two properties are encapsulated in a new class StockID.
My classes definitions are:
public class Producto
{
public virtual long Id { get; set;
public virtual Stock Stock { get; set; }
... Some other (not so important ) properties ...
public Producto()
{
...
}
}
public class Stock
{
public virtual StockID ID { get; set; }
public virtual Producto ProductoStock { get; set; }
... other properties ...
}
public class StockID
{
public virtual string Token { get; set; }
public virtual long CodigoInterno { get; set; }
public override int GetHashCode()
{
int hash = GetType().GetHashCode();
hash = (hash * 31) ^ CodigoInterno.GetHashCode();
hash = (hash * 31) ^ Token.GetHashCode();
return hash;
}
public override bool Equals(object obj)
{
var other = obj as StockID;
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return this.CodigoInterno == other.CodigoInterno &&
this.Token == other.Token;
}
}
And these are the maps:
public class ProductoMap : ClassMap<Producto>
{
public ProductoMap()
{
Id(x => x.Id);
// ... Other Maps and References
References<Stock>( p => p.Stock);
}
}
public class StockMap : ClassMap<Stock>
{
public StockMap()
{
CompositeId( stock => stock.ID)
.KeyProperty(x => x.CodigoInterno)
.KeyProperty(x => x.Token);
// ... Other Maps and References
References(x => x.ProductoStock);
}
}
This is the Exception I get...
Foreign key (FKD33BD86ADE26BE17:Producto [Stock_id])) must have same number of columns as the referenced primary key (Stock [CodigoInterno, Token])
How can I fix this?
You have to map the property that matches your class Producto with your class Stock. It should look like this:
public class StockMap : ClassMap<Stock>
{
public StockMap()
{
CompositeId( stock => stock.ID)
.KeyProperty(x => x.CodigoInterno)
.KeyProperty(x => x.Token);
// ... Other Maps and References
//This is ok since you have (i believe) a column in
//in your table that stores the id from Producto
References(x => x.ProductoStock).Column("idProducto");
//Maybe you have to put the name of the column with
//the id of Producto in your table Stock because
//you could use it later.
}
}
And the ProductMap class:
public class ProductoMap : ClassMap<Producto>
{
public ProductoMap()
{
Id(x => x.Id);
// ... Other Maps and References
//If you have the id from Stock in your Stock class
//this will work.
//References<Stock>( p => p.Stock);
//If you don't have it, this will work:
HasOne<Stock>(x => x.Stock).PropertyRef("Name of the column whit the product id in the Stock table");
}
}
The second choice seem to be better from my point of view. Anyway, if you have a legacy database o something like that is not a really big difference.
I have a DB structure which is not ideal, but I have coded it into EF like this:
[Table("Item")]
public class Item
{
[Key] public int Id { get; set; }
public int CategoryId { get; set; }
public int ItemTypeId { get; set; } // An ItemTypeId of 1 means this row refers to an Article
public int ItemId { get; set; } // this refers to the Article primary key
}
[Table("Article")]
public class Article
{
[Key] public int Id { get; set; }
...
public virtual ICollection<SubArticle> SubArticles { get; set; }
}
[Table("SubArticle")]
public class SubArticle
{
...
public int ArticleId { get; set; }
}
modelBuilder.Entity<Article>().Collection(_ => _.SubArticles).InverseReference(_ => _.Article).ForeignKey(_ => _.ArticleId);
What I want to do is get all articles (with the corresponding sub-articles) that belong to a specific category. I have this query which is working:
var result = await Context.Set<Item>()
.Where(i => i.CategoryId == 200)
.Where(i => i.ItemTypeId == 1)
.Join(
Context.Set<Article>().Include(a => a.SubArticles),
i => i.ItemId,
a => a.Id,
(i,a) => a)
.ToListAsync();
result.SubArticles.First(); // works
My question is why does this query not work:
var result = await Context.Set<Item>()
.Where(i => i.CategoryId == 200)
.Where(i => i.ItemTypeId == 1)
.Join(
Context.Set<Article>(),
i => i.ItemId,
a => a.Id,
(i,a) => a)
.Include(a => a.SubArticles)
.ToListAsync();
result.SubArticles.First(); // error: result.SubArticles is null
I have a table called InvestigatorGroup and a table called InvestigatorGroupUsers which is used to see what groups have what users. I am trying to get the common investigator group between two users
My query is as follows:
public InvestigatorGroup GetCommonGroup(string userId, string investigatorUserId)
{
using (GameDbContext entityContext = new GameDbContext())
{
string[] ids = new[] { userId, investigatorUserId };
return entityContext.InvestigatorGroups
.Where(i => i.IsTrashed == false)
.Include(i => i.InvestigatorGroupUsers)
.Where(i => i.InvestigatorGroupUsers.Any(e => ids.Contains(e.UserId)))
.OrderByDescending(i => i.InvestigatorGroupId)
.GroupBy(i => i.InvestigatorGroupId)
.Where(i => i.Count() > 1)
.SelectMany(group => group).FirstOrDefault();
}
}
The entity InvestigatorGroup is as follows:
public class InvestigatorGroup : IIdentifiableEntity
{
public InvestigatorGroup()
{
this.InvestigatorGroupGames = new HashSet<InvestigatorGroupGame>();
this.InvestigatorGroupUsers = new HashSet<InvestigatorGroupUser>();
}
// Primary key
public int InvestigatorGroupId { get; set; }
public string InvestigatorGroupName { get; set; }
public bool HasGameAssignment { get; set; }
public string GroupRoleName { get; set; }
public bool IsTrashed { get; set; }
// Navigation property
public virtual ICollection<InvestigatorGroupUser> InvestigatorGroupUsers { get; private set; }
public virtual ICollection<InvestigatorGroupGame> InvestigatorGroupGames { get; private set; }
public int EntityId
{
get { return InvestigatorGroupId; }
set { InvestigatorGroupId = value; }
}
}
The problem is that it keeps returning a value of 0. It doesn't see the shared group with a count of 2 between the two users.
I did a test to return the groups (I removed the count>1 condition) and it returned all the groups for both users not only the one they have in common
I believe the issue is with this line: .Where(i => i.InvestigatorGroupUsers.Any(e => ids.Contains(e.UserId)))
Thanks for the help!
I've resolved this by changing my query so that it searches for the rows containing one of the UserId's. Then it queries through those selected rows and selects the ones containing the other UserId (InvestigatorUserId). This way only the rows containing both are returned
My new code is as follows:
public InvestigatorGroup GetCommonGroup(string userId, string investigatorUserId)
{
using (GameDbContext entityContext = new GameDbContext())
{
IEnumerable<InvestigatorGroup> userGroups = entityContext.InvestigatorGroups
.Where(i => i.IsTrashed == false)
.Include(i => i.InvestigatorGroupUsers)
.Where(i => i.InvestigatorGroupUsers.Any(e => e.UserId.Contains(userId)))
.OrderByDescending(i => i.InvestigatorGroupId);
return userGroups.Where(i => i.InvestigatorGroupUsers.Any(e => e.UserId.Contains(investigatorUserId))).FirstOrDefault();
}
}
I'm building a wiki, which has articles that consists of subarticles. An article can consist of multiple subarticles, and a subarticle can be attached to multiple articles. In the jointable there is a sortorder that defines the display of subarticles for a particular article.
The parent-article only consists of a title (and metadata), no text, all text is done through subarticles.
This sortorder in the jointable though, is where I'm stuck atm, I can't access it from my query. Hopefully someone can point me in the right direction.
Sidenote: I'm quite new in the whole MVC/EF world, even c#/vb and .NET is something I've only been working on since a few months and in my spare time.
I have these classes:
Article:
public class Article : BaseEntity
{
private ICollection<Category> _categories;
private ICollection<ArticleSubarticle> _subarticles;
public string Title { get; set; }
public int AuthorId { get; set; }
public DateTime CreationDate { get; set; }
public DateTime ?PublishDate { get; set; }
public DateTime ?ChangeDate { get; set; }
public bool Published { get; set; }
public virtual ICollection<Category> Categories
{
get { return _categories ?? (_categories = new List<Category>()); }
protected set { _categories = value; }
}
public virtual ICollection<ArticleSubarticle> Subarticles
{
get { return _subarticles ?? (_subarticles = new List<ArticleSubarticle>()); }
protected set { _subarticles = value; }
}
}
Subarticle
public class Subarticle : Article
{
private ICollection<Attachment> _attachments;
public string ArticleText { get; set; }
public int OriginalArticle { get; set; }
public bool Active { get; set; }
public virtual ICollection<Attachment> Attachments
{
get { return _attachments ?? (_attachments = new List<Attachment>()); }
protected set { _attachments = value; }
}
}
Jointable:
public class ArticleSubarticle : BaseEntity
{
public int ParentId { get; set; }
public int ChildId { get; set; }
public int SortOrder { get; set; }
public virtual Article Parent { get; set; }
public virtual Subarticle Child { get; set; }
}
They are mapped as follows:
Article
public ArticleMap () {
ToTable("Wiki_Article");
HasKey(a => a.Id);
Property(a => a.Title).HasColumnType("VARCHAR").HasMaxLength(250);
Property(a => a.AuthorId);
Property(a => a.PublishDate).IsOptional();
Property(a => a.ChangeDate).IsOptional();
HasMany(a => a.Categories)
.WithMany()
.Map(a => a.ToTable("Wiki_Article_Category_Mapping"));
}
Subarticle
public SubarticleMap()
{
ToTable("Wiki_Subarticle");
HasKey(sa => sa.Id);
Property(a => a.ArticleText)
.IsOptional()
.HasColumnType("TEXT");
Property(a => a.OriginalArticle)
.IsOptional();
HasMany(a => a.Attachments)
.WithMany()
.Map(a => a.ToTable("Wiki_Subarticle_Attachment_Mapping"));
}
Jointable
public ArticleSubarticleMap()
{
ToTable("Wiki_Article_Subarticle_Mapping");
HasKey(asa => new { asa.ParentId, asa.ChildId });
HasRequired(asa => asa.Parent)
.WithMany(asa => asa.Subarticles)
.HasForeignKey(asa => asa.ParentId);
}
This gets me the database as expected.
Now I want an article with its subarticles, that are ordered by the sortorder.
This query gets me the article with its subarticles, but I can't seem to figure out how to reach this sortorder in the Wiki_Article_Subarticle_Mapping table.
public IList<Article> getArticleByIdWithSortedSubarticles(int ArticleId)
{
var query = _articleRepository.Table;
query = query.Where(a => ArticleId == a.Id)
.Select(a => a);
var subarticles = query.ToList();
return subarticles;
}
Any ideas?
Thanks in advance!
Your query is not loading subarticles currently, so I guess they are lazily loaded. Try loading them explicitly like this:
public IList<Article> getArticleByIdWithSortedSubarticles(int ArticleId)
{
var query = _articleRepository.Table;
query = query.Where(a => ArticleId == a.Id)
.Select(a => new { article = a, subs = a.SubArticles.OrderBy(s => s.SortOrder) });
return query.AsEnumerable().Select(m => m.article).ToList();
}