I have a parent object book, and a property of that object is publisher. Everytime I ad a book, it is adding a new publisher, even if the publisher already exists. Can someone tell me how to add the book and instead of adding the publisher again, just reference an existing one? The code i am using is below... Thanks in advance!
public class Book
{
public int BookID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreateDate { get; set; }
public virtual Publisher Publisher { get; set; }
}
public class Publisher
{
public int PublisherID { get; set; }
public string Address { get; set; }
}
public class SqlCEDataStore : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Publishers> Publishers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.IncludeMetadataInDatabase = false;
}
}
public class TimeSinkRepository : IRepository<Book>
{
private static SqlCEDataStore context = new SqlCEDataStore();
public int Add(Book entity)
{
context.Books.Add(entity);
return context.SaveChanges();
}
}
var book = new Book()
{
Title = "New Title",
Description = "New Description",
CreateDate = DateTime.Now,
Publisher = new Publisher() { PublisherID = 1 }
};
var repository = new BookRepository();
var result = repository.Add(book);
The problem is in the line:
Publisher = new Publisher() { PublisherID = 1 }
Object context doesn't know that this is existing publisher. It is newly created entity so Object context will perform insert operation. You have to say object context that the publisher object is not newly created. One way to do that is modification of your Add method:
public int Add(Book entity)
{
context.Books.Add(entity);
// 0 means new one, other values mean existing one
if (entity.Publisher.PublisherID > 0)
{
context.ObjectStateManager.ChangeObjectState(entity.Publisher, EntityState.Unchanged);
}
context.SaveChanges();
}
It you can solve this by making sure the Publisher is attached to Publishers context before adding the Book entity (this way it knows it's a Publisher from the dbcontext and not a new one that it needs to add (again))
context.Publishers.Attach(book.Publisher); // This is only possible if the Publisher is not new
context.Books.Add(book);
the problem is in this line
Publisher = new Publisher() { PublisherID = 1 }
You should do a fetch method so something like this
- Get the Publisher you want from the context (eg where id = 1)
- Set the returned object as the publisher for your new book object
- The context should sort the rest out for you. when you save the book. (no need to mess with the object state manager)
Good luck, if you cant get this working put up some code of it and i will help you though it.
Related
I have a Kalem Entity with a collection of DigerKalemMaliyetleri property, which is a collection of MaliyetBirimi objects. DigerKalemMaliyetleri is of JSON type and stored at the same table as a JSON column.
public class Kalem
{
public int Id { get; set; }
[Column(TypeName = "json")]
public ICollection<MaliyetBirimi> DigerKalemMaliyetleri { get; set; }
}
public class MaliyetBirimi
{
public int? DovizCinsi { get; set; }
public decimal? Maliyet { get; set; }
}
When I try to update entity with only DigerKalemMaliyetleri property changed:
DataContext.Entry<Kalem>(first).CurrentValues.SetValues(second);
SQL Update command isn't executed and database record is not updated.
How could I update the entity without explicitly setting DigerKalemMaliyetleri property?
Regards
I had the same problem, you cann't actually use SetValues to update navigation property, you nead instead use DataContext.Update(YourNewObj) and then DataContext.SaveChanges();, or if you want to use SetValues approach, you need:
-Get the exist entry
Kalem existObj = DataContext.Kalems.Find(YourNewObj.Id);
-Loop in navigations of updating entry and the existing one to set the values of updating entry:
foreach(var navObj in DataContext.Entry(YourNewObj).Navigations)
{
foreach(var navExist in DatatContext.Entry(existObj).Navigations)
{
if(navObj.Metadata.Name == navExist.MetaData.Name)
navExist.CurrentValue = navObj.CurrentValue;
}
}
-Update also changes of direct properties:
DataContext.Entry(existObj).CurrentValues.SetValues(YourNewObj);
-Save your Updating:
DataContext.SaveChanges();
You can also check if you need to load your Navigations before going in foreach loop, otherwise you will get an error.
Please if you see beter scenario, correct me.
It's hard to know exactly what you're doing without a complete code sample. Note also that you're trying to set all properties of first from second, including e.g. the Id, which is probably not what you want.
Here's a complete code sample which works for me:
await using (var ctx = new BlogContext())
{
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();
ctx.Kalem.Add(new()
{
DigerKalemMaliyetleri = new List<MaliyetBirimi>()
{
new() { DovizCinsi = 1, Maliyet = 2 }
}
});
await ctx.SaveChangesAsync();
}
await using (var ctx = new BlogContext())
{
var first = ctx.Kalem.Find(1);
var second = new Kalem
{
DigerKalemMaliyetleri = new List<MaliyetBirimi>()
{
new() { DovizCinsi = 3, Maliyet = 4 }
}
};
ctx.Entry(first).Property(k => k.DigerKalemMaliyetleri).CurrentValue = second.DigerKalemMaliyetleri;
await ctx.SaveChangesAsync();
}
public class BlogContext : DbContext
{
public DbSet<Kalem> Kalem { get; set; }
static ILoggerFactory ContextLoggerFactory
=> LoggerFactory.Create(b => b.AddConsole().AddFilter("", LogLevel.Information));
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseNpgsql(#"Host=localhost;Username=test;Password=test")
.EnableSensitiveDataLogging()
.UseLoggerFactory(ContextLoggerFactory);
}
public class Kalem
{
public int Id { get; set; }
[Column(TypeName = "json")]
public ICollection<MaliyetBirimi> DigerKalemMaliyetleri { get; set; }
}
public class MaliyetBirimi
{
public int? DovizCinsi { get; set; }
public decimal? Maliyet { get; set; }
}
I am fairly new to Entity Framework and investigating converting some legacy data access code to using EF. I want to know if the following is possible in EF and if yes how.
Say I have a Customer table like this
CustomerId | ProductId | StartDate | EndDate
--------------------------------------------
100 | 999 | 01/01/2012| null
Say I also load Product data from somewhere else (like an XML file) as a cache of product objects.
public class Customer
{
public int CustomerId {get;set;}
public int Product {get;set}
public DateTime StartDate {get;set;}
public DateTime? EndDate {get;set;}
}
public class Product
{
public int ProductId {get;set;}
public int Description {get;set}
}
Currently in CustomerDal class the method uses a StoredProc to get a Customer object like this
Customer GetCustomer(int customerId)
{
// setup connection, command, parameters for SP, loop over datareader
Customer customer = new Customer();
customer.CustomerId = rdr.GetInt32(0);
int productId = rdr.GetInt32(1);
// ProductCache is a singleton object that has been initialised before
customer.Product = ProductCache.Instance.GetProduct(productId);
customer.StartDate = rdr.GetDateTime(2);
customer.EndDate = rdr.IsDbNull(3) ? (DateTime?)null : rdr.GetDateTime(3);
return customer;
}
My question is this possible using EF when it materializes the Customer object it sets the Product property not from the DB but by another method, in this case from an in memory cache. Similary when saving a new Customer object it only gets the ProductId from the Products property and saves the value in DB.
If you attach your product instances to the EF context then when loading a Customer the Product property will be automatically filled from memory without a query to database as long as the product that is associated to the customer is already attached.
For example, starting with these entities:
public class Customer
{
public int Id { get; set; }
public int ProductId { get; set; }
public string Name { get; set; }
public Product Product { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Description { get; set; }
}
Products will be available globally, for simplicity, lets make it a static class:
public static class CachedProducts
{
public static Product[] All
{
get
{
return new Product[] { new Product { Id = 1, Description = "Foo" } };
}
}
}
With this in mind we just need to assure that every EF context starts with all the products attached to it:
public class CustomerContext : DbContext
{
public CustomerContext()
{
// Attach products to context
Array.ForEach(CachedProducts.All, p => this.Products.Attach(p));
}
public DbSet<Customer> Customers { get; set; }
public DbSet<Product> Products { get; set; }
}
And finally, to make the sample complete and runnable we seed the database, request a customer and print the associated product description:
public class DatabaseInitializer : CreateDatabaseIfNotExists<CustomerContext>
{
protected override void Seed(CustomerContext context)
{
var p = new Product { Id = 1, Description = "Foo" };
var c = new Customer { Id = 1, Product = p, Name = "John Doe" };
context.Customers.Add(c);
context.SaveChanges();
}
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer<CustomerContext>(new DatabaseInitializer());
using (var context = new CustomerContext())
{
var customer = context.Customers.Single(c => c.Id == 1);
Console.WriteLine(customer.Product.Description);
}
}
}
If you attach a profiler to SQL Server you will notice that the customer is loaded from database but no query is performed to obtain the product since it is already attached to the context. This works when loading a customer and also when saving a new customer with an associated product.
Disclaimer: I'm not an EF expert so this approach may have some undesired side effects that I'm unable to consider.
I am currently using EF4.3 and Code First. Creation of my objects works (via my views - just using the auto-generated Create), but when I attempt to edit an object, it does not save any changes that, utlimately, tie back to my navigation properties. I have been reading on relationships, but I don't understand how to tell my context that the relationship has changed.
Here is some example code of my implementation.
#* Snippet from my view where I link into my ViewModel. *#
<div class="row">
<div class="editor-label">
#Html.LabelFor(model => model.ManagerID)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.ManagerID, ViewBag.Manager as SelectList, String.Empty)
#Html.ValidationMessageFor(model => model.ManagerID)
</div>
</div>
Here is my Controller implementation (POST of my Edit):
[HttpPost]
public ActionResult Edit(ProjectViewModel projectViewModel)
{
if (ModelState.IsValid)
{
Project project = new Project();
project.ProjectID = projectViewModel.ProjectID;
project.Name = projectViewModel.Name;
project.ProjectManager = repository.GetUser(projectViewModel.ManagerID);
repository.InsertOrUpdateProject(project);
repository.Save();
return RedirectToAction("Index");
}
ViewBag.Manager = new SelectList(repository.GetUsers(), "UserID", "FullName", projectViewModel.ManagerID);
return View(projectViewModel);
}
Within my Project object:
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
// Navigation Properties
public virtual User Manager { get; set; }
}
Here is the corresponding method from the repository (where my context resides):
public void InsertOrUpdateProject(Project project)
{
if (program.ProjectID == default(int))
{
context.Projects.Add(project);
}
else
{
context.Entry(project).State = EntityState.Modified;
}
}
Just to be clear, this does work to update my properties, but it does not update my navigation properties (in this case, Manager). Appreciate any help.
Setting the state to Modified only marks scalar properties as modified, not navigation properties. You have several options:
A hack (you won't like it)
//...
else
{
var manager = project.Manager;
project.Manager = null;
context.Entry(project).State = EntityState.Modified;
// the line before did attach the object to the context
// with project.Manager == null
project.Manager = manager;
// this "fakes" a change of the relationship, EF will detect this
// and update the relatonship
}
Reload the project from the database including (eager loading) the current manager. Then set the properties. Change tracking will detect a change of the manager again and write an UPDATE.
Expose a foreign key property for the Manager navigation property in your model:
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
public int ManagerID { get; set; }
public virtual User Manager { get; set; }
}
Now ManagerID is a scalar property and setting the state to Modified will include this property. Moreover you don't need to load the Manager user from the database, you can just assign the ID you get from your view:
Project project = new Project();
project.ProjectID = projectViewModel.ProjectID;
project.Name = projectViewModel.Name;
project.ManagerID = projectViewModel.ManagerID;
repository.InsertOrUpdateProject(project);
repository.Save();
There are several options here, I will list 3 of them:
Option 1: Using GraphDiff
*This needs the Configuration.AutoDetectChangesEnabled of your context set to true.
Just install GraphDiff with NuGet
Install-Package RefactorThis.GraphDiff
Then
using (var context = new Context())
{
var customer = new Customer()
{
Id = 12503,
Name = "Jhon Doe",
City = new City() { Id = 8, Name = "abc" }
};
context.UpdateGraph(customer, map => map.AssociatedEntity(p => p.City));
context.Configuration.AutoDetectChangesEnabled = true;
context.SaveChanges();
}
For more details about GraphDiff look here.
Option 2: Find and Edit
Searching your entity with EF to track it to the context. Then edit the properties.
*This needs the Configuration.AutoDetectChangesEnabled of your context set to true.
var customer = new Customer()
{
Id = 12503,
Name = "Jhon Doe",
City = new City() { Id = 8, Name = "abc" }
};
using (var context = new Contexto())
{
var customerFromDatabase = context.Customers
.Include(x => x.City)
.FirstOrDefault(x => x.Id == customer.Id);
var cityFromDataBase = context.Cities.FirstOrDefault(x => x.Id == customer.City.Id);
customerFromDatabase.Name = customer.Name;
customerFromDatabase.City = cityFromDataBase;
context.Configuration.AutoDetectChangesEnabled = true;
context.SaveChanges();
}
Option 3: Using a scalar property
In a matter of performance this is the best way, but it mess your class with database concerns. Because you will need to create a scalar (primitive type) property to map the Id.
*In this way there is no need to set the Configuration.AutoDetectChangesEnabled to true. And also you won't need to do a query to the database to retrieve the entities (as the first two options would - yes GraphDiff does it behind the scenes!).
var customer = new Customer()
{
Id = 12503,
Name = "Jhon Doe",
City_Id = 8,
City = null
};
using (var contexto = new Contexto())
{
contexto.Entry(customer).State = EntityState.Modified;
contexto.SaveChanges();
}
I am not sure exactly what you mean by navigation properties? Do you mean like a foreign key relationship? If so then try the following data annotation:
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
[ForeignKey("YourNavigationProperty")]
public virtual UserManager { get; set; }
}
Update your EF Context, and see what happens?
UPDATE
public class Project
{
public int ProjectID { get; set; }
[Required]
public string Name { get; set; }
[ForeignKey("ManagerId")]
public ManagerModel UserManager { get; set; }
}
public class ManagerModel
{
[Key]
public int ManagerId { get; set; }
public String ManagerName { get; set; }
}
See if that works?
I have found myself with at little problem, and I think a custom model binder is the way to go.
My Domain model looks like this,readly standard
I got a Page and a Template. The Page has the Template as a ref.
So the Default asp.net mvc Binder, does not know how to bind it, therefore I need to make some rules for it. (Custom Model Binder)
public class PageTemplate
{
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual DateTime? Created { get; set; }
public virtual DateTime? Modified { get; set; }
}
public class Page
{
public virtual string Title { get; set; }
public virtual PageTemplate Template { get; set; }
public virtual string Content { get; set; }
public virtual DateTime? Created { get; set; }
public virtual DateTime? Modified { get; set; }
}
So I have Registreted the ModelBinder in globals.asax
ModelBinders.Binders.Add(typeof(Cms.Domain.Entities.Page),
new Cms.UI.Web.App.ModelBinders.PageModelBinder(
new Services.GenericApplicationService<Cms.Domain.Entities.Page>().GetEntityStore()
)
);
My ModelBinder tage a paremeter, witch is my Repository, where I get all my Entities ( Page, Template )
My Controller for a Page looks like this.
I have posted into a Create Controler, it does not matter for now, if it was a Update method.
Since I in this case have a dropdown, that represents the Template, I will get an ID in my form collection.
I then call: TryUpdateModel and I got a hit in my PageModelBinder.
[AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Create(FormCollection form)
{
Page o = new Page();
string[] exclude = new { "Id" }
if (base.TryUpdateModel<Page>(o, string.Empty, null, exclude, form.ToValueProvider()))
{
if (ModelState.IsValid)
{
this.PageService.Add(o);
this.CmsViewData.PageList = this.PageService.List();
this.CmsViewData.Messages.AddMessage("Page is updated.", MessageTypes.Succes);
return View("List", this.CmsViewData);
}
}
return View("New", this.CmsViewData);
}
So I end op with the Model Binder.
I have search the internet dry for information, but im stock.
I need to get the ID from the FormCollection, and parse it to at Model from my IEntityStore.
But how ?
public class PageModelBinder : IModelBinder
{
public readonly IEntityStore RepositoryResolver;
public PageModelBinder(IEntityStore repositoryResolver)
{
this.RepositoryResolver = repositoryResolver;
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
if (modelType == typeof(Cms.Domain.Entities.Page))
{
// Do some magic
// Get the Id from Property and bind it to model, how ??
}
}
}
// Dennis
I hope, my problom is clear.
Did find a work around.
I download the sourcecode for asp.net r2 rtm 2
And did copy all code for the default ModelBinder, and code it need. Did some minor change, small hacks.
the work around is doing a little hack in this method:
[SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
Justification = "The target object should make the correct culture determination, not this method.")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification = "We're recording this exception so that we can act on it later.")]
private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType)
{
try
{
object convertedValue = valueProviderResult.ConvertTo(destinationType);
return convertedValue;
}
catch (Exception ex)
{
try
{
// HACK if the binder still fails, try get the entity in db.
Services.GenericApplicationService<Cms.Domain.Entities.PageTemplate> repo;
repo = new Services.GenericApplicationService<Cms.Domain.Entities.PageTemplate>();
int id = Convert.ToInt32(valueProviderResult.AttemptedValue);
object convertedValue = repo.Retrieve(id);
return convertedValue;
}
catch (Exception ex1)
{
modelState.AddModelError(modelStateKey, ex1);
return null;
}
}
}
This question is closed.
I'm new to EF 4.0, so maybe this is an easy question. I've got VS2010 RC and the latest EF CTP. I'm trying to implement the "Foreign Keys" code-first example on the EF Team's Design Blog, http://blogs.msdn.com/efdesign/archive/2009/10/12/code-only-further-enhancements.aspx.
public class Customer
{
public int Id { get; set;
public string CustomerDescription { get; set;
public IList<PurchaseOrder> PurchaseOrders { get; set; }
}
public class PurchaseOrder
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public DateTime DateReceived { get; set; }
}
public class MyContext : ObjectContext
{
public RepositoryContext(EntityConnection connection) : base(connection){}
public IObjectSet<Customer> Customers { get {return base.CreateObjectSet<Customer>();} }
}
I use a ContextBuilder to configure MyContext:
{
var builder = new ContextBuilder<MyContext>();
var customerConfig = _builder.Entity<Customer>();
customerConfig.Property(c => c.Id).IsIdentity();
var poConfig = _builder.Entity<PurchaseOrder>();
poConfig.Property(po => po.Id).IsIdentity();
poConfig.Relationship(po => po.Customer)
.FromProperty(c => c.PurchaseOrders)
.HasConstraint((po, c) => po.CustomerId == c.Id);
...
}
This works correctly when I'm adding new Customers, but not when I try to retrieve existing Customers. This code successfully saves a new Customer and all its child PurchaseOrders:
using (var context = builder.Create(connection))
{
context.Customers.AddObject(customer);
context.SaveChanges();
}
But this code only retrieves Customer objects; their PurchaseOrders lists are always empty.
using (var context = _builder.Create(_conn))
{
var customers = context.Customers.ToList();
}
What else do I need to do to the ContextBuilder to make MyContext always retrieve all the PurchaseOrders with each Customer?
You could also use:
var customers = context.Customers.Include("PurchaseOrders").ToList();
Or enable LazyLoading in the ContextOptions :
context.ContextOptions.LazyLoadingEnabled = true;
Just be careful with deferred loading if you are serializing the objects or you may end up querying the entire database.
Well the solution turned out to be simple, as I suspected it might. I called the context.LoadProperty() method for each individual customer:
using (var context = _builder.Create(_conn))
{
var customers = context.Customers.ToList();
foreach (var customer in customers)
{
context.LoadProperty<Customer>(customer, c => c.PurchaseOrders);
}
return customers;
}