Entity Framework Core import / export tree of entities with similar foreign keys - asp.net-core

I need to add an import/export functionality to my ASP.NET Core application.
What I would like is to take entities in one database, export these entities into one file, and then import that file into a new database.
My problem is that I have some entities that hold same foreign key. Here is a simple model illustrating what I want to do:
public class Bill
{
public List<Product> Products { get; set; }
public int Id { get; set; }
...
}
public class Product
{
public int Id { get; set; }
public int ProductCategoryId { get; set; }
public ProductCategory ProductCategory { get; set; }
...
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
I want to export a bill to be imported on an other environment of my application. So if I export the bill, I will get a Json like this:
{
"id": 1,
"products" : [
{
"id" : 1,
"productCategoryId": 1,
"productCategory": {
"id" : 1,
"name" : "Category #1"
}
},
{
"id" : 2,
"productCategoryId": 1,
"productCategory": {
"id" : 1,
"name" : "Category #1"
}
},
{
"id" : 3,
"productCategoryId": 1,
"productCategory": {
"id" : 2,
"name" : "Category #2"
}
}
]
}
If I deserialize this json into entities in my new environment (ignoring Ids mapping of course), I will get three new categories (category will be duplicated for product 1 and 2) because the serializer will instanciate two categories...
So when I push it into my database, it will add 3 lines instead 2 into the Category table...
Thanks in advance for your answers.

Suppose you have a list of Category to be imported, you could firstly get the id list of these categories and then query the database to make sure which has been already stored in database. And for those already existing ones, just skip them (or update them as you like).
Since we have multiple entity types (categories,products,bills and potential BillProducts), rather than writing a new importer for each TEntity , I prefer to writing a generic Importer method to deal with any Entity type list with Generic and Reflection :
public async Task ImportBatch<TEntity,TKey>(IList<TEntity> entites)
where TEntity : class
where TKey: IEquatable<TKey>
{
var ids = entites.Select( e=> GetId<TEntity,TKey>(e));
var existingsInDatabase=this._context.Set<TEntity>()
.Where(e=> ids.Any(i => i.Equals(GetId<TEntity,TKey>(e)) ))
.ToList();
using (var transaction = this._context.Database.BeginTransaction())
{
try{
this._context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT " + typeof(TEntity).Name + " ON;");
this._context.SaveChanges();
foreach (var entity in entites)
{
var e= existingsInDatabase.Find(existing => {
var k1 =GetId<TEntity,TKey>(existing);
var k2=GetId<TEntity,TKey>(entity);
return k1.Equals(k2);
});
// if not yet exists
if(e == null){
this._context.Add(entity);
}else{
// if you would like to update the old one when there're some differences
// uncomment the following line :
// this._context.Entry(e).CurrentValues.SetValues(entity);
}
}
await this._context.SaveChangesAsync();
transaction.Commit();
}
catch{
transaction.Rollback();
}
finally{
this._context.Database.ExecuteSqlCommand($"SET IDENTITY_INSERT " + typeof(TEntity).Name + " OFF;");
await this._context.SaveChangesAsync();
}
}
return;
}
Here the GetId<TEntity,TKey>(TEntity e) is a simple helper method which is used to get the key filed of e:
// use reflection to get the Id of any TEntity type
private static TKey GetId<TEntity,TKey>(TEntity e)
where TEntity : class
where TKey : IEquatable<TKey>
{
PropertyInfo pi=typeof(TEntity).GetProperty("Id");
if(pi == null) { throw new Exception($"the type {typeof(TEntity)} must has a property of `Id`"); }
TKey k = (TKey) pi.GetValue(e);
return k ;
}
To make the code more reusable, we can create an EntityImporter service to hold the method above :
public class EntityImporter{
private DbContext _context;
public EntityImporter(DbContext dbContext){
this._context = dbContext;
}
public async Task ImportBatch<TEntity,TKey>(IList<TEntity> entites)
where TEntity : class
where TKey: IEquatable<TKey>
{
// ...
}
public static TKey GetId<TEntity,TKey>(TEntity e)
where TEntity : class
where TKey : IEquatable<TKey>
{
// ...
}
}
and then register the services at startup time:
services.AddScoped<DbContext, AppDbContext>();
services.AddScoped<EntityImporter>();
Test Case :
Firstly, I'll take several categories as an example :
var categories = new ProductCategory[]{
new ProductCategory{
Id = 1,
Name="Category #1"
},
new ProductCategory{
Id = 2,
Name="Category #2"
},
new ProductCategory{
Id = 3,
Name="Category #3"
},
new ProductCategory{
Id = 2,
Name="Category #2"
},
new ProductCategory{
Id = 1,
Name="Category #1"
},
};
await this._importer.ImportBatch<ProductCategory,int>(categories);
The expected results should be only 3 rows imported :
1 category #1
2 category #2
3 category #3
And here's a screenshot it works:
Finally, for your bills json, you can do as below to import the entites :
var categories = bill.Products.Select(p=>p.ProductCategory).ToList();
var products = bill.Products.ToList();
// other List<TEntity>...
// import the categories firstly , since they might be referenced by other entity
await this._importer.ImportBatch<ProductCategory,int>(categories);
// import the product secondly , since they might be referenced by BillProduct table
await this._import.ImportBatch<Product,int>(products);
// ... import others

Related

EFPlus BulkInsert - How to get DB-generated IDs

Using MSSQL with IDENTITY column for IDs,
how can I get entity IDs synchronized with table IDs after calling BulkInsert?
context.BulkInsert(entities);
Neither of both achieves the requested result:
context.BulkSynchronize(entities);
context.BulkMerge(entities);
Assume we have one entity
var newSomething = new Something { Id = 0 };
and the corresponding TSQL table column definition
ID int IDENTITY(1,1)
Entity Framework automatically sets Id after calling SaveChanges()
context.SomethingSet.Add(newSomething);
context.SaveChanges();
Assert.IsTrue(newSomething.Id != 0)
See also How can I get Id of inserted entity in Entity framework?
How does EFPlus provide a way of getting the Id of inserted entities?
Disclaimer: I'm the owner of the project Entity Framework Extensions
The Entity Framework Extensions library should by default already return the ids for inserted entities.
For example, the following code should already work and return ids when using with BulkInsert.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Windows.Forms;
namespace Z.EntityFramework.Extensions.Lab
{
public partial class Form_Request_Ids : Form
{
public Form_Request_DateNull()
{
InitializeComponent();
// CLEAR
using (var ctx = new CurrentContext())
{
ctx.EntitySimples.RemoveRange(ctx.EntitySimples);
ctx.SaveChanges();
}
// TEST
using (var ctx = new CurrentContext())
{
var list = new List<EntitySimple>();
list.Add(new EntitySimple() { Id = 0, IntColumn = 1, CreatedDate = DateTime.Now });
ctx.BulkInsert(list);
}
}
public class CurrentContext : DbContext
{
public CurrentContext()
: base("CodeFirstEntities")
{
}
public DbSet<EntitySimple> EntitySimples { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Types().Configure(x => x.ToTable(GetType().DeclaringType != null ? GetType().DeclaringType.FullName.Replace(".", "_") + "_" + x.ClrType.Name : ""));
base.OnModelCreating(modelBuilder);
}
}
public class EntitySimple
{
public int Id { get; set; }
public int IntColumn { get; set; }
public DateTime CreatedDate { get; set; }
}
}
}
If you still have the issue, try to contact us directly with an example info#zzzprojects.com or post your example here.

How to change the collection name on an index

When I save a document that has a generic type DataView<Customer>, I'm manually setting the collection name to "customers". However, I'm having some trouble making an index using AbstractIndexCreationTask with a non-default collection name. Here's my index:
public class customers_Search
: AbstractIndexCreationTask<DataView<Customer>, customers_Search.Result>
{
public class Result
{
public string Query { get; set; }
}
public customers_Search()
{
Map = customers =>
from customer in customers
where customer.Data != null
select new
{
Query = AsDocument(customer.Data).Select(x => x.Value)
};
Index(x => x.Query, FieldIndexing.Analyzed);
}
}
When this gets deployed, it looks like this:
from customer in docs.DataViewOfCustomer
where customer.Data != null
select new {
Query = customer.Data.Select(x => x.Value)
}
This doesn't work obviously, and if I change DataViewOfCustomer to "customers" it works just fine.
I'd rather not have to use non-type-checked (string) indexes to deploy. Is there a way to set the collection name that from the AbstractIndexCreationTask class?
Update
Since my data class is generic, I made a generic index which fixes up the names.
public class DataViewQuery<TEntity>
: AbstractIndexCreationTask<DataView<TEntity>, DataViewQueryResult>
{
private readonly string _entityName;
private readonly string _indexName;
// this is to fix the collection name for the index name
public override string IndexName { get { return _indexName; } }
// this is to fix the collection name for the index query
public override void Execute(IDatabaseCommands databaseCommands, DocumentConvention documentConvention)
{
var conventions = documentConvention.Clone();
conventions.FindTypeTagName =
type =>
typeof(DataView<TEntity>) == type
? _entityName
: documentConvention.FindTypeTagName(type);
base.Execute(databaseCommands, conventions);
}
public DataViewQuery(string entityName)
{
_entityName = entityName;
_indexName = String.Format("{0}/{1}", entityName, "Query");
Map = items =>
from item in items
where item.Data != null
select new
{
Query = AsDocument(item.Data).Select(x => x.Value)
};
Index(x => x.Query, FieldIndexing.Analyzed);
}
}
public class DataViewQueryResult
{
public string Query { get; set; }
}
Then I can create a specific index which has all the configuration in it.
// sets the collection type (DataView<Customer>) for the index
public class CustomerQuery : DataViewQuery<Customer>
{
// sets the collection name for the index
public CustomerQuery() : base(EntityName.Customers) { }
}
You need to configure this in the conventions.
The property to configure is FindTypeTagName

Returning sub-properties from document in RavenDb via Index using Lucene

I'm using RavenDB 2.5 and what I want to do is query a Group (see below) providing a valid Lucene search term and get back a collection of Member instances (or even just Ids) that match. So, class definition:
public class Group {
public string Id { get; set; }
public IList<Member> Members { get; set; }
}
public class Member {
public string Name { get; set; }
public string Bio { get; set; }
}
And they are stored in the database as session.Store(groupInstance); as you'd expect. What I'd like to do is query and return the Member instances which match a given search term.
So, something like:
public class GroupMembers_BySearchTerm : AbstractIndexCreationTask {
public override IndexDefinition CreateIndexDefinition(){
return new IndexDefinition {
Map = "from g in docs.Groups select new { Content = new [] { g.Members.Select(m => m.Name), g.Members.Select(m => m.Bio) }, Id = g.Id }",
Indexes = { { "Id", FieldIndexing.Default }, { "Content", FieldIndexing.Analyzed } }
}
}
}
If I call this using something like:
session.Advanced.LuceneQuery<Group, GroupMembers_BySearchTerm>().Where("Id: myId AND Content: search terms").ToList();
I obviously get back a Group instance, but how can I get back the Members instead?
What about an index like this:
public class Members_BySearchTermAndGroup : AbstractIndexCreationTask {
public override IndexDefinition CreateIndexDefinition(){
return new IndexDefinition {
Map = "from g in docs.Groups
from member in g.Members
select new {
GroupdId = g.Id,
Name = member.Name,
Bio = member.Bio,
Content = new [] {member.Name, member.Bio },
}",
Indexes = {
{ "GroupId", FieldIndexing.Default },
{ "Content", FieldIndexing.Analyzed }
},
Stores = {
{ "Name", FieldStorage.Yes },
{ "Bio", FieldStorage.Yes }
}
}
}
}
If you take a closer look you'll see that we are creating a new lucene entry for each member inside of a group. Consequently, you'll be able to query on those elements and retrieve them.
Finally you can query your store like this (more info about searching):
session.Query<Member, Members_BySearchTermAndGroup>()
.Search(x => x.Content, "search terms")
.ProjectFromIndexFieldsInto<Member>()
.ToList();
I cannot check this right now but I guess that you need to project your results using the ProjectFromIndexFieldsInto(). Some more information about projections in this link.
or, following your example:
session.Advanced
.LuceneQuery<Member, Members_BySearchTermAndGroup>()
.Where("GroupId: myId AND Content: search terms")
.SelectFields<Member>()
.ToList();

NHibernate table per type persist child from existing parent

I have 2 classes, Member.cs and Customer.cs and using table-per-type inheritance mapping described here.
This question poses the same problem, but with no answer.
Customer.cs
public class Customer
{
}
Member.cs
public class Member : Customer
{
public Member(Customer customer)
{
CreateFromCustomer(customer);
}
private void CreateFromCustomer(Customer customer)
{
// Here I assume I'll assign the Id so NHibernate wouldn't have to create a new Customer and know what Customer to be referred
Id = customer.Id;
}
}
CustomerMap.cs
public class CustomerMap : ClassMap<Customer>
{
public CustomerMap()
{
Id(x => x.Id)
.GeneratedBy.GuidComb();
}
}
MemberMap.cs
public class MemberMap : SubclassMap<Member>
{
public MemberMap()
{
KeyColumn("Id");
}
}
I tried several test case :
Test1.cs
[Test]
public void CanAddCustomer()
{
var customerRepo = /* blablabla */;
using (var tx = NHibernateSessionManager.GetSession().BeginTransaction())
{
var customer = new Customer()
customerRepo.RegisterCustomer(customer);
tx.Commit();
}
using (var tx = NHibernateSessionManager.GetSession().BeginTransaction())
{
/* Get the persisted customer */
var customer = customerRepo.GetCustomerByWhatever();
var member = customerRepo.RegisterMember(new Member(customer));
tx.Commit();
}
}
I'm expecting to have :
1 customer and 1 member which is a child of that customer
Instead I have :
2 customers (1 that is what was correctly created and 1 with all null columns) and 1 member that Id referred to all null columns Customer.
Is it the expected behavior?
I understand if we were wanted to create a child object from a transient parent object, this is a correct behavior.
But what if we were to create a child object that refers to an existing parent object?
The link I provided doesn't cover any persistence example, neither does googling.
Short Answer
No, it is not possible to "upgrade" an already persisted object to its subclass. Nhibernate simply doesn't support this. That's why you see 2 customers and one member entry. This is actually the expected behavior because Nhibernate simply creates a copy with a new ID of the object instead of creating the reference to Member...
So basically you could do either
Copy the data of Customer into Member, delete customer and save Member
Use a different object structure without subclasses where Member is a different table with it's own ID and a reference to Customer
Use native sql to insert the row into Member...
Some example:
Your classes could look like this
public class Customer
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
}
public class Member : Customer
{
public virtual string MemberSpecificProperty { get; set; }
}
Basically, Member could have additional properties, but will have the same properties as Customer of cause, too.
public class CustomerMap : ClassMap<Customer>
{
public CustomerMap()
{
Id(x => x.Id)
.GeneratedBy.GuidComb();
Map(x => x.Name);
}
}
and for the sub class, you have to map additional properties only!
public class MemberMap : SubclassMap<Member>
{
public MemberMap()
{
Map(x => x.MemberSpecificProperty);
}
}
testing it
{
session.Save(new Customer()
{
Name ="Customer A"
});
session.Save(new Member()
{
Name = "Customer B",
MemberSpecificProperty = "something else"
});
session.Flush();
}
This will create 2 entries in the customer table and one row into Member table. So this is as expected, because we created one customer and one member...
Now the "upgrade" from Customer A to Member:
using (var session = NHibernateSessionFactory.Current.OpenSession())
{
session.Save(new Customer()
{
Name ="Customer A"
});
session.Flush();
}
using (var session = NHibernateSessionFactory.Current.OpenSession())
{
var customer = session.Query<Customer>().FirstOrDefault();
//var member = customer as Member;
var member = new Member()
{
Name = customer.Name,
MemberSpecificProperty = "something else"
};
session.Delete(customer);
session.Save(member);
session.Flush();
}

ADO.NET Entity Framework Hierarchical Data

Consider following database model:
And following query code:
using (var context = new DatabaseEntities())
{
return context.Feed.ToHierarchy(f => f.Id_Parent, null);
}
Where ToHierarchy is an extension to ObjectSet<TEntity> as:
public static List<TEntity> ToHierarchy<TEntity, TProperty>(this ObjectSet<TEntity> set, Func<TEntity, TProperty> parentIdProperty, TProperty idRoot) where TEntity : class
{
return set.ToList().Where(f => parentIdProperty(f).Equals(idRoot)).ToList();
}
This would result in example JSON formatted response:
[
{
"Description":"...",
"Details":[ ],
"Id":1,
"Id_Parent":null,
"Title":"...",
"URL":"..."
},
{
"Description":"...",
"Details":[
{
"Description":"...",
"Details":[ ],
"Id":4,
"Id_Parent":3,
"Title":"...",
"URL":"..."
},
{
"Description":"...",
"Details":[
{
"Description":"...",
"Details":[
{
"Description":"...",
"Details":[ ],
"Id":7,
"Id_Parent":6,
"Title":"...",
"URL":"..."
}
],
"Id":6,
"Id_Parent":5,
"Title":"...",
"URL":"..."
}
],
"Id":5,
"Id_Parent":3,
"Title":"...",
"URL":null
}
],
"Id":3,
"Id_Parent":null,
"Title":"...",
"URL":null
}
]
As you may have noticed ToHierarchy method is supposed to (and apparently do indeed) retrieve all rows from a given set (flat) and return hierarchical representation of these as per "parent property".
When I was in the middle of my implementation I quick tried my code and surprisingly it worked! Now, I imagine how weird does this sound to many of you, but I really don't understand why or how that piece of code works, even though I kinda wrote it down on my own...
Could you explain how does it work?
P.S.: if you look closer, ToHierarchy is not near the same as .Include("Details").
It works because set.ToList will load all records from the database table to your application and the rest is done be EF and its change tracking mechanism which should ensure correct referencing between related entities.
Btw. you are filtering records in the memory of your application, not in the database. For example if your table contains 10000 records and your filter should return only 10, you will still load all 10000 from the database.
You will find that implementing this with EF is quite hard because EF has no support for hierarchical data. You will always end with bad solution. The only good solution is using stored procedure and some support for hierarchical queries in the database - for example common table expressions (CTE) in SQL server.
I just made this very simple example and it works as I described in comment:
public class SelfReferencing
{
public int Id { get; set; }
public string Name { get; set; }
public SelfReferencing Parent { get; set; }
public ICollection<SelfReferencing> Children { get; set; }
}
public class Context : DbContext
{
public DbSet<SelfReferencing> SelfReferencings { get; set; }
}
public class Program
{
static void Main(string[] args)
{
using (var context = new Context())
{
context.Database.Delete();
context.Database.CreateIfNotExists();
context.SelfReferencings.Add(
new SelfReferencing()
{
Name = "A",
Children = new List<SelfReferencing>()
{
new SelfReferencing()
{
Name = "AA",
Children = new List<SelfReferencing>()
{
new SelfReferencing()
{
Name = "AAA"
}
}
},
new SelfReferencing()
{
Name = "AB",
Children = new List<SelfReferencing>()
{
new SelfReferencing()
{
Name = "ABA"
}
}
}
}
});
context.SaveChanges();
}
using (var context = new Context())
{
context.Configuration.LazyLoadingEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
var data = context.SelfReferencings.ToList();
}
}
}
It uses code first approach but internally it is same as when using EDMX. When I get data I have 5 entities in list and all have correctly configured Parent and Children navigation properties.