How to map only changed properties using Automapper and Entity Framework - entity-framework-core

I know that this question had been asked already, but those answers didn't work for me. So, I beg not to close this question! 😎
The goal is simple:
Get entity from database using Entity Framework.
Pass this entity to MVC view.
Edit entity on the page.
Save entity.
What I have:
Entities
β€’ Client relates to table in database.
β€’ ClientDto is the same as Client, but without Comment property (to emulate situation when user must not see this field).
public class Client
{
public string TaskNumber { get; set; }
public DateTime CrmRegDate { get; set; }
public string Comment { get; set; }
}
public class ClientDto
{
public string TaskNumber { get; set; }
public DateTime CrmRegDate { get; set; }
}
Database table
CREATE TABLE dbo.client
(
task_number varchar(50) NOT NULL,
crm_reg_date date NOT NULL,
comment varchar(3000) NULL
);
-- Seeding
INSERT INTO dbo.client VALUES
('1001246', '2010-09-14', 'comment 1'),
('1001364', '2010-09-14', 'comment 2'),
('1002489', '2010-09-22', 'comment 3');
MVC controller
public class ClientsController : Controller
{
private readonly ILogger logger;
private readonly TestContext db;
private IMapper mapper;
public ClientsController(TestContext db, IMapper mapper, ILogger<ClientsController> logger)
{
this.db = db;
this.logger = logger;
this.mapper = mapper;
}
public IActionResult Index()
{
var clients = db.Clients;
var clientsDto = mapper.ProjectTo<ClientDto>(clients);
return View(model: clientsDto);
}
public async Task<IActionResult> Edit(string taskNumber)
{
var client = await db.Clients.FindAsync(taskNumber);
return View(model: client);
}
[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
// For now it's empty - it'll be used for saving entity
}
}
Mappings:
builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>();
}
Helper (outputs entity entry as JSON)
internal static class EntititesExtensions
{
internal static string ToJson<TEntity>(this EntityEntry<TEntity> entry) where TEntity: class
{
var states = from member in entry.Members
select new
{
State = Enum.GetName(member.EntityEntry.State),
Name = member.Metadata.Name,
Value = member.CurrentValue
};
var json = JsonSerializer.SerializeToNode(states);
return json.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
}
}
THE PROBLEM
I need to map only changed properties from ClientDto to Client in Edit(ClientDto clientDto).
Which solutions were used
Solution 1
Just map ClientDto to Client:
[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
var client = mapper.Map<Client>(clientDto);
logger.LogInformation(db.Entry(client).ToJson());
db.Update(client);
await db.SaveChangesAsync();
return View(viewName: "Success");
}
The problem with this code is that absolutely new Client entity gets created which properties will be filled by ClientDto. This means that Comment property will be NULL and it will make NULL the column comment in database. In general case, all hidden properties (i.e. absent in DTO) will have their default values. Not good.
Output:
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
Solution 2
I tried to use the solution from the answer I mentioned above:
public static IMappingExpression<TSource, TDestination> MapOnlyIfChanged<TSource, TDestination>(this IMappingExpression<TSource, TDestination> map)
{
map.ForAllMembers(source =>
{
source.Condition((sourceObject, destObject, sourceProperty, destProperty) =>
{
if (sourceProperty == null)
return !(destProperty == null);
return !sourceProperty.Equals(destProperty);
});
});
return map;
}
In configuration:
builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>().MapOnlyIfChanged();
});
Running same code as in solution 1, we get the same output (Comment is null):
[
{
"State": "Detached",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Detached",
"Name": "Comment",
"Value": null
},
{
"State": "Detached",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
Not good.
Solution 3
Let's take another route:
Obtain entity from database.
Use non-generic Map in order to overwrite the values from ClientDto to Client from database.
Tune mapping configuration in order to skip properties which values are equal.
builder.Services
.AddAutoMapper(config =>
{
config.CreateMap<Client, ClientDto>();
config.CreateMap<ClientDto, Client>()
.ForMember(
dest => dest.TaskNumber,
opt => opt.Condition((src, dest) => src.TaskNumber != dest.TaskNumber))
.ForMember(
dest => dest.TaskNumber,
opt => opt.Condition((src, dest) => src.CrmRegDate != dest.CrmRegDate));
});
[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
var dbClient = await db.Clients.FindAsync(clientDto.TaskNumber);
logger.LogInformation(db.Entry(dbClient).ToJson());
mapper.Map(
source: clientDto,
destination: dbClient,
sourceType: typeof(ClientDto),
destinationType: typeof(Client)
);
logger.LogInformation(db.Entry(dbClient).ToJson());
db.Update(dbClient);
await db.SaveChangesAsync();
return View(viewName: "Success");
}
This finally works, but it has problem - it still modifies all properties. Here's the output:
[
{
"State": "Modified",
"Name": "TaskNumber",
"Value": "1001246"
},
{
"State": "Modified",
"Name": "Comment",
"Value": "comment 1"
},
{
"State": "Modified",
"Name": "CrmRegDate",
"Value": "2010-09-15T00:00:00"
}
]
So, how to make Automapper update only modified properties? 😐

You do not need to call context.Update() explicitly.
When loading entity, EF remember every original values for every mapped property.
Then when you change property, EF will compare current properties with original and create appropriate update SQL only for changed properties.
For further reading: Change Tracking in EF Core

Related

Use Smart contracts mint function in nethereum with parameters

I try to build a game in unity, in which you can earn a blockchain token by minting it.
The API of the mint function looks like this:
{
"constant": false,
"inputs": [{"internalType": "address","name": "account","type": "address"}, {"internalType": "uint256","name": "amount","type": "uint256"}],
"name": "mint",
"outputs": [{"internalType": "bool","name": "","type": "bool"}],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
I first checked the approach in the flappy-bird-documentation and tried to change the set to score as follows:
public Function mintToken() {
return contract.GetFunction("mint");
}
CreateMintTokenTransactionInput(string addressFrom, string addressOwner, string privateKey, string adress , BigInteger amount, HexBigInteger gas = null, HexBigInteger valueAmount = null) {
var function = mintToken ();
return function.CreateTransactionInput(addressFrom, gas, valueAmount, adress, amount);
}
In the Start method of unity I go:
var transactionInput = CreateMintTokenTransactionInput(_userAddress, _addressOwner, _privateKey, adress, amount);
var transactionSignedRequest = new TransactionSignedUnityRequest(_url, GameControl.instance.Key, _userAddress);
//send and wait
yield return transactionSignedRequest.SignAndSendTransaction(transactionInput);
But the parameters of the function seem to be wrong.
I wonder if there is an easier way to do this or how I could fix my approach.
In more general terms: How can I call an arbitrary smart contract method with parameters in Ethereum?
The next Problem ist that i dont get a transactionHash and I am not sure how to Debug this:
yield return transactionMintRequest.SignAndSendTransaction(mint, contractAddress);
if (transactionMintRequest.Exception != null)
{
Debug.Log(transactionMintRequest.Exception.Message);
yield break;
}
Debug.Log(transactionMintRequest);
var transactionMintHash = transactionMintRequest.Result;
Debug.Log("Transfer txn hash:" + transactionMintHash);
Following the documentation http://docs.nethereum.com/en/latest/unity3d-smartcontracts-getting-started/#transfer-transaction
The simplest thing to do is to create first your Mint Function, this has been code generated using the http://playground.nethereum.com/
public partial class MintFunction : MintFunctionBase { }
[Function("mint", "bool")]
public class MintFunctionBase : FunctionMessage
{
[Parameter("address", "account", 1)]
public virtual string Account { get; set; }
[Parameter("uint256", "amount", 2)]
public virtual BigInteger Amount { get; set; }
}
Then create your TransactionSignedUnityRequest as follows:
var transactionMintRequest = new TransactionSignedUnityRequest(url, privateKey, "YOURCHAINID");
var mint = new MintFunction
{
Account= account,
Amount = amount,
};
yield return transactionMintRequest.SignAndSendTransaction(mint, contractAddress);
var transactionMintHash = transactionMintRequest.Result;

How to Make a app acording to language change

I developed an app that can support two languages. Going beyond that if I need this app for support with many languages. let assume ten languages how to accomplish this requirement
Now what I do is Hard cording it with both languages I used and then retrieve it. If I going to do that for ten languages it does not sound good is there any smart way to do this.
as an example;
I doing with MVVM pattern In my View Model. I use the property and do this its simple way of doing
public string GetPageTitle => AppResources.VitalSignsViewPage_Title;
and my resource file I have two data set if the language has changed it will use write dataset this is what I am doing right now is there any proper smart way to this to many languages.
There is some solutions to achive this. I recommend to you basic
solution ,
Here is an interface for Language Service (It is optionally , if you
using Dependency Injection):
public interface ILanguageService
{
string GetString(string text);
void ChangeLanguage(bool isALang);
bool IsALanguage();
}
You can create a service for localization :
namespace Service.Language
{
public sealed class LanguageService : ILanguageService
{
List<LanguageRow> LanguageList;
private bool IsFirstLang;
public LanguageService()
{
LanguageList = JsonHelper.ReadJSON<List<LanguageRow>>("Service.Language.MultiLanguage.json", typeof(LanguageService));
IsFirstLang = true;
}
public void ChangeLanguage(bool IsFirstLang)
{
IsFirstLang = !IsFirstLang;
}
public bool IsALangueage()
{
return IsFirstLang;
}
public string GetString(string text)
{
string result;
try
{
var row = LanguageList.FirstOrDefault(i => i.Code.Equals(text));
result = IsFirstLang? row.Values[0] : row.Values[1];
}
catch
{
result = text;
}
return result;
}
}
}
Here is a model to serialization for json :
public class LanguageRow
{
public LanguageRow()
{
Values = new List<string>();
}
public string Code { get; set; }
public List<string> Values { get; set; }
}
At last, here is json file : (EN-FR)
[
{
"Code": "VitalSignsViewPage_Title",
"Values": [ "Page Title", "Titre de la page" ]
},
{
"Code": "VitalSignsViewPage_SubTitle",
"Values": [ "Sub Title", "Sous-titre" ]
},
{
"Code": "VitalSignsViewPage_SubSubTitle",
"Values": [ "Sub Sub Title", "Sous sous-titre" ]
}
]
You can access translations like :
ILanguageService _langService = new LangService()
_langService.GetString(AppResources.VitalSignsViewPage_Title);

ASP.NET 5 WebAPI

I am creating API post methods in ASP.NET 5 webapi. I have a requirement that I need to create overloaded post methods as follows.
[HttpPost]
public bool LogInfo([FromBody]LogEventModel value)
{
}
[HttpPost]
public bool LogInfo (String strInformation)
{
}
I tried multiple options to set the routing parameters as follows.
[HttpPost("LogInfo/{strInformation}")]
(or)
[Route("LogInfo/{strInformation}")]
But its not working. I hope I am messing up something here. Can anyone help ?
I don't think this is possible anymore. Because of the fact that Controllers and Web API Controllers are now the same thing, routing has been unified also. You can read more about it here.
So what I would do is, this is my controller code:
public class HomeController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
ViewBag.Title = $#"{nameof(HomeController)}-{nameof(Index)}";
return View();
}
[HttpPost]
[Route("home/setsomething2", Name = "SetSomething2")]
public void SetSomething([FromBody]SomeValueModel someValue)
{
var value = someValue;
}
[HttpPost]
[Route("home/setsomething1", Name = "SetSomething1")]
public void SetSomething(String someValue)
{
var value = someValue;
}
}
public class SomeValueModel
{
[JsonProperty("somevalue")]
public string SomeValue { get; set; }
}
And this is how I called it from the view Index.cshtml:
function postSomething1() {
var url = "#Url.RouteUrl("SetSomething1", new {someValue = "someValue"})";
$.post(url)
.done(function () {
alert("Succes!");
})
.fail(function (response) {
console.log(response);
alert("Fail!");
});
}
function postSomething2() {
var url = "#Url.RouteUrl("SetSomething2")";
$.ajax({
contentType: 'application/json',
data: JSON.stringify({ "somevalue": "myvalue" }),
dataType: 'json',
success: function () {
alert("Succes!");
},
error: function () {
alert("Error!");
},
processData: false,
type: 'POST',
url: url
});
}
You could however make the routing of SetSomething2 more RESTful, like:
[Route("home/setsomething/{someValue}", Name = "SetSomething2")]
Note: Notice how the path 'home/setsomething/' is cleaned from numbers, so this would be my prefered way to go.
And if you REALLY don't want to make different routes you can't use named routes. So you'll have to ditch the names like:
[Route("home/setsomething")]
And:
[Route("home/setsomething/{someValue}")]
And then call them in jQuery like for instance:
var url = "home/setsomething/somevalue";
Here is what I do.
I prefer specifying routes for each controller by annotating them with:
[Route("[controller]/[action]")]
Then an action may look like:
[HttpPost]
[ActionName("GetSomeDataUsingSomeParameters")]
public List<Thing> GetSomeDataUsingSomeParameters([FromBody] MyParameters parms)
{
return Repo.GetData(parms);
}
The request may look like:
http://myhost/mycontroller/GetSomeDataUsingSomeParameters
The parameters are passed as a Json structure in the body by the POST method. For example:
{"Parameter1":1,"Parameter2":"a string"}
The request should also specify:
Content-Type: application/json
So let's say your class Thing is:
public class Thing
{
public int Id { get; set; }
public string Name { get; set; }
}
Then the response could be (with two things found in the database):
[{"Id":1,"Name":"First thing"},{"Id":2,"Name":"Another thing"}]

ASP.NET Web Api CRUD operation in VS 2010 web application

I tried to make ASP.NET Web Api CRUD operation in VS 2010 web application, but why the result is not returning all entire row from source table.
This is my code :
Route/Globax.asax
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}", // browse with localhost:7031/api/product
//routeTemplate: "{controller}/{id}", // browse with localhost:7031/product
defaults: new { id = System.Web.Http.RouteParameter.Optional }
);
Controller/ProductController.cs :
public class ProductController : ApiController
{
NorthwindEntities db = new NorthwindEntities();
public List<Product> GetAll()
{
return db.Products.ToList<Product>();// ;
}
View/ViewProduct.aspx :
<script src="Script/jquery-1.7.1.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
$('#<%= cviewproduct.ClientID %>').click(function (e) {
getProducts();
e.preventDefault();
});
});
function getProducts() {
$.getJSON("/api/product",
function (data) {
$.each(data, function (key, val) {
//var str = val.ProductName;
// alert(str);
var row = '<tr> <td>' + val.ProductName + '</td><td>' + val.ProductID + '</td><tr/>';
$(row).appendTo($('#tblproduct'));
});
});
}
</script>
Bellow Is The Result of product Controller via 'http://localhost:7031/api/product' :
Bellow Is The Result of getProducts() function :
Please help me.
Any idea or suggestion ?
When you execute $.getJSON("/api/product", ..., you are not getting back XML as you posted. You are getting back JSON.
As a first step, I suggest you download and install Fiddler2. Open it, and use the Composer tab to execute a GET for http://localhost:7031/api/product. That should show you the JSON returned, which will be different from the XML. Post that JSON to your original question and we should be able to help further.
My guess is that the JSON is not properly formatted. What does your WebApiConfig.cs class look like?
Update
Yes, there is a better way. First, create an ApiModel (ApiModel is to WebAPI what ViewModel is to MVC):
public class ProductApiModel
{
public int ProductId { get; set; }
public string ProductName { get; set; }
}
Now, return this instead of an entity from your controller:
public class ProductController : ApiController
{
public IEnumerable<ProductApiModel> GetAll()
{
var products = db.Products
.OrderBy(x => x.ProductID)
.Select(x => new ProductApiModel
{
ProductId = x.ProductID,
ProductName = x.ProductName
});
return products;
}
}
If you used AutoMapper, you could make your controller code a bit shorter:
public class ProductController : ApiController
{
public IEnumerable<ProductApiModel> GetAll()
{
var entities = db.Products.OrderBy(x => x.ProductID);
var models = Mapper.Map<ProductApiModel[]>(entities);
return models;
}
}
After do some research, i finaly found (Please Correct Me, if i'm wrong) :
JavaScriptSerializer failed to serialize the entire objects tree from relations
between the entity and other entities (Product and Catagory).
So .. I made ​​some changes to my code, and the results are as expected
Controller/ProductController.cs :
From :
public class ProductController : ApiController
{
NorthwindEntities db = new NorthwindEntities();
public List<Product> GetAll()
{
return db.Products.ToList<Product>();// ;
}
To :
public class ProductController : ApiController
{
public string GetAll()
{
var product = db.Products.OrderBy(x => x.ProductID).Select(x => new
{
ProductId = x.ProductID,
ProductName = x.ProductName
});
return JsonConvert.SerializeObject(product);
}
View/ViewProduct.aspx :
From :
function getProducts() {
$.getJSON("/api/product",
function (data) {
$.each(data, function (key, val) {
var row = '<tr> <td>' + val.ProductName
+ '</td><td>' + val.ProductID + '</td><tr/>';
$(row).appendTo($('#tblproduct'));
});
});
To :
$.getJSON("/api/product",
function (data) {
var obj = $.parseJSON(data);
$.each(obj, function (key, val) {
var row = '<tr> <td>' + val.ProductId
+ '</td><td>' + val.ProductName + '</td><tr/>';
$(row).appendTo($('#tblproduct'));
});
});
Or... is there a better way than this,
if I still want to pass an entity object instead of a string object
(List GetAll ()... instead GetAll string ()....)
Regards,
Andrian
So.. This My Final Working Code
Models :
namespace MyLabs1.Models
{
public class ProductApi
{
[Key]
public Int32 ProductID { get; set; }
public string ProductName { get; set; }
}
}
Controller/ProductController.cs :
public IEnumerable<ProductApi> getall()
{
Mapper.CreateMap<Product, ProductApi>();
var entities = db.Products.OrderBy(x => x.ProductID).ToList();
var models = Mapper.Map<ProductApi[]>(entities);
return models;
}
OR
public List<ProductApi> GetAll()
{
var products = db.Products
.OrderBy(x => x.ProductID)
.Select(x => new ProductApi
{
ProductID = x.ProductID,
ProductName = x.ProductName
}).ToList();
return products;
}
View/ViewProduct.aspx :
function getProducts() {
$.getJSON("/api/product",
function (data) {
$.each(data, function (key, val) {
var row = '<tr> <td>' + val.ProductName + '</td><td>' + val.ProductID + '</td><tr/>';
$(row).appendTo($('#tblproduct'));
});
});
}
Hope This Will Be Helpful to Others
Regards,
Andrian

How to seed data with many-to-may relations in Entity Framework Migrations

I use entity framework migration (in Automatic migration mode). Everything is okay, but I have one question:
How should I seed data when I have many-to-many relationships?
For example, I have two model classes:
public class Parcel
{
public int Id { get; set; }
public string Description { get; set; }
public double Weight { get; set; }
public virtual ICollection<BuyingItem> Items { get; set; }
}
public class BuyingItem
{
public int Id { get; set; }
public decimal Price { get; set; }
public virtual ICollection<Parcel> Parcels { get; set; }
}
I understand how to seed simple data (for PaymentSystem class) and one-to-many relationships, but what code should I write in the Seed method to generate some instances of Parcel and BuyingItem? I mean using DbContext.AddOrUpdate(), because I don't want to duplicate data every time I run Update-Database.
protected override void Seed(ParcelDbContext context)
{
context.AddOrUpdate(ps => ps.Id,
new PaymentSystem { Id = 1, Name = "Visa" },
new PaymentSystem { Id = 2, Name = "PayPal" },
new PaymentSystem { Id = 3, Name = "Cash" });
}
protected override void Seed(Context context)
{
base.Seed(context);
// This will create Parcel, BuyingItems and relations only once
context.AddOrUpdate(new Parcel()
{
Id = 1,
Description = "Test",
Items = new List<BuyingItem>
{
new BuyingItem() { Id = 1, Price = 10M },
new BuyingItem() { Id = 2, Price = 20M }
}
});
context.SaveChanges();
}
This code creates Parcel, BuyingItems and their relationship, but if I need the same BuyingItem in another Parcel (they have a many-to-many relationship) and I repeat this code for the second parcel - it will duplicate BuyingItems in the database (though I set the same Ids).
Example:
protected override void Seed(Context context)
{
base.Seed(context);
context.AddOrUpdate(new Parcel()
{
Id = 1,
Description = "Test",
Items = new List<BuyingItem>
{
new BuyingItem() { Id = 1, Price = 10M },
new BuyingItem() { Id = 2, Price = 20M }
}
});
context.AddOrUpdate(new Parcel()
{
Id = 2,
Description = "Test2",
Items = new List<BuyingItem>
{
new BuyingItem() { Id = 1, Price = 10M },
new BuyingItem() { Id = 2, Price = 20M }
}
});
context.SaveChanges();
}
How can I add the same BuyingItem in different Parcels?
Updated Answer
Make sure you read "Using AddOrUpdate Properly" section below for a complete answer.
First of all, let's create a composite primary key (consisting of parcel id and item id) to eliminate duplicates. Add the following method in the DbContext class:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Parcel>()
.HasMany(p => p.Items)
.WithMany(r => r.Parcels)
.Map(m =>
{
m.ToTable("ParcelItems");
m.MapLeftKey("ParcelId");
m.MapRightKey("BuyingItemId");
});
}
Then implement the Seed method like so:
protected override void Seed(Context context)
{
context.Parcels.AddOrUpdate(p => p.Id,
new Parcel { Id = 1, Description = "Parcel 1", Weight = 1.0 },
new Parcel { Id = 2, Description = "Parcel 2", Weight = 2.0 },
new Parcel { Id = 3, Description = "Parcel 3", Weight = 3.0 });
context.BuyingItems.AddOrUpdate(b => b.Id,
new BuyingItem { Id = 1, Price = 10m },
new BuyingItem { Id = 2, Price = 20m });
// Make sure that the above entities are created in the database
context.SaveChanges();
var p1 = context.Parcels.Find(1);
// Uncomment the following line if you are not using lazy loading.
//context.Entry(p1).Collection(p => p.Items).Load();
var p2 = context.Parcels.Find(2);
// Uncomment the following line if you are not using lazy loading.
//context.Entry(p2).Collection(p => p.Items).Load();
var i1 = context.BuyingItems.Find(1);
var i2 = context.BuyingItems.Find(2);
p1.Items.Add(i1);
p1.Items.Add(i2);
// Uncomment to test whether this fails or not, it will work, and guess what, no duplicates!!!
//p1.Items.Add(i1);
//p1.Items.Add(i1);
//p1.Items.Add(i1);
//p1.Items.Add(i1);
//p1.Items.Add(i1);
p2.Items.Add(i1);
p2.Items.Add(i2);
// The following WON'T work, since we're assigning a new collection, it'll try to insert duplicate values only to fail.
//p1.Items = new[] { i1, i2 };
//p2.Items = new[] { i2 };
}
Here we make sure that the entities are created/updated in the database by calling context.SaveChanges() within the Seed method. After that, we retrieve the required parcel and buying item objects using context. Thereafter we use the Items property (which is a collection) on the Parcel objects to add BuyingItem as we please.
Please note, no matter how many times we call the Add method using the same item object, we don't end up with primary key violation. That is because EF internally uses HashSet<T> to manage the Parcel.Items collection. A HashSet<Item>, by its nature, won't let you add duplicate items.
Moreover, if you somehow manage to circumvent this EF behavior as I have demonstrated in the example, our primary key won't let the duplicates in.
Using AddOrUpdate Properly
When you use a typical Id field (int, identity) as an identifier expression with AddOrUpdate method, you should exercise caution.
In this instance, if you manually delete one of the rows from the Parcel table, you'll end up creating duplicates every time you run the Seed method (even with the updated Seed method I have provided above).
Consider the following code,
context.Parcels.AddOrUpdate(p => p.Id,
new Parcel { Id = 1, Description = "Parcel 1", Weight = 1.0 },
new Parcel { Id = 2, Description = "Parcel 1", Weight = 1.0 },
new Parcel { Id = 3, Description = "Parcel 1", Weight = 1.0 }
);
Technically (considering the surrogate Id here), the rows are unique, but from the end-user point of view, they are duplicates.
The true solution here is to use the Description field as an identifier expression. Add this attribute to the Description property of the Parcel class to make it unique: [MaxLength(255), Index(IsUnique=true)]. Update the following snippets in the Seed method:
context.Parcels.AddOrUpdate(p => p.Description,
new Parcel { Description = "Parcel 1", Weight = 1.0 },
new Parcel { Description = "Parcel 2", Weight = 2.0 },
new Parcel { Description = "Parcel 3", Weight = 3.0 });
// Make sure that the above entities are created in the database
context.SaveChanges();
var p1 = context.Parcels.Single(p => p.Description == "Parcel 1");
Note, I'm not using the Id field as EF is going to ignore it while inserting rows. And we are using Description to retrieve the correct parcel object, no matter what Id value is.
Old Answer
I would like to add a few observations here:
Using Id is probably not going to do any good if the Id column is a database generated field. EF is going to ignore it.
This method seems to be working fine when the Seed method is run once. It won't create any duplicates, however, if you run it for a second time (and most of us have to do that often), it may inject duplicates. In my case it did.
This tutorial by Tom Dykstra showed me the right way of doing it. It works because we don't take anything for granted. We don't specify IDs. Instead, we query the context by known unique keys and add related entities (which again are acquired by querying context) to them. It worked like a charm in my case.
You must fill many-to-many relation in the same way as you build many-to-many relation in any EF code:
protected override void Seed(Context context)
{
base.Seed(context);
// This will create Parcel, BuyingItems and relations only once
context.AddOrUpdate(new Parcel()
{
Id = 1,
Description = "Test",
Items = new List<BuyingItem>
{
new BuyingItem() { Id = 1, Price = 10M },
new BuyingItem() { Id = 2, Price = 20M }
}
});
context.SaveChanges();
}
Specifying Id which will be used in database is crucial otherwise each Update-Database will create new records.
AddOrUpdate doesn't support changing relations in any way so you cannot use it to add or remove relations in next migration. If you need it you must manually remove relation by loading Parcel with BuyingItems and calling Remove or Add on navigation collection to break or add new relation.
Ok. I understand how I should be in that situation:
protected override void Seed(Context context)
{
base.Seed(context);
var buyingItems = new[]
{
new BuyingItem
{
Id = 1,
Price = 10m
},
new BuyingItem
{
Id = 2,
Price = 20m,
}
}
context.AddOrUpdate(new Parcel()
{
Id = 1,
Description = "Test",
Items = new List<BuyingItem>
{
buyingItems[0],
buyingItems[1]
}
},
new Parcel()
{
Id = 2,
Description = "Test2",
Items = new List<BuyingItem>
{
buyingItems[0],
buyingItems[1]
}
});
context.SaveChanges();
}
There are no duplicates in database.
Thank you, Ladislav, you gave me a right vector to find a solution for my task.