How to add data while inserting or updating entities - entity-framework

We're using RIA Services in our Silverlight app, and for one of our entities we want to track who creates and update them and when. For this we've added these properties:
public class Person
{
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public string LastModifiedBy { get; set; }
public DateTime LastModifiedOn { get; set; }
}
We would like to update these values in the domain service so that we don't have to do this on the client (and because entitities will also be added/updated server side(. I tried to do it by modified the domain service method like this:
public void InsertPerson(Person person)
{
person.CreatedBy = GetCurrentUser();
person.CreatedOn = DateTime.Now();
DbEntityEntry<Person> entityEntry = this.DbContext.Entry(person);
if ((entityEntry.State != EntityState.Detached))
{
entityEntry.State = EntityState.Added;
}
else
{
this.DbContext.Persons.Add(person);
}
}
public void UpdatePerson(Person person)
{
person.LastModifiedBy = GetCurrentUser();
person.LastModifiedOn = DateTime.Now();
DbContext.Persons.AttachAsModified(person, ChangeSet.GetOriginal(person), DbContext);
}
but that didn't seem to add this data at all. I then tried to do it with sql queries after inserting/updating entities with
DbContext.Database.ExecuteSqlCommand("UPDATE Persons SET LastModifiedById = {0}, LastModifiedOn = {1} where Id = {2}", GetCurrentUser(), DateTime.Now, person.Id);
which actually updates the database, but the client is not updated/notified of the changes until the entities is fetch from the database again.
Does anyone have a good idea of how to best achieve this?

yes call the
DBContext.SaveChanges()
to actually commit the changes into the database

Related

ASP.NET Core MVC. Implementation of the IDataProtectionKeyContext interface

There is a data context:
public class OurDbContext : DbContext, IOurDbContext, IDataProtectionKeyContext
{
public DbSet<Employee> Employees { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } = null!;
}
There is a method that implements sending data to the database:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> CreateNewClient(Employee client, string TypeOfClient)
{
var secstring = _protector.Protect(client.Password);
Employee temp = new Employee
{
Name = client.Name,
Password = secstring,
RoleId = 1
};
await _mediatr.Send(new NewEmployee.NewEmployeeCommand(temp));
return Redirect("~/");
}
Nothing gets into the database.
If you remove the implementation from the context class IDataProtectionKeyContext
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } = null!;
and shorten the line with
builder.Services.AddDataProtection().PersistKeysToDbContext<OurDbContext>();
before
builder.Services.AddDataProtection();
then the data gets into the database with an encrypted password. But in this case, after 5 minutes, an attempt to read this password will cause an exception due to an outdated key.
The Microsoft help doesn't say anything about this:
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-6.0#persistkeystodbcontext
In short, I tried to change the context from PostgreSQL to MS SQL and Sqlite, and everything works well in them with the configuration that Microsoft recommends in its help.

EF Core tracking problem when adding Entity to a List

I ran into a problem while developing my small Blazor WASM app.
A part of my app is where users can create teams, and invite other users to join their team. The relevant Entity Classes is:
Team.cs
public class Team
{
[Key]
public Guid TeamID { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
public Guid? BadgeID { get; set; }
public Guid TownID { get; set; }
public Guid StatisticsID { get; set; }
public Guid CaptainID { get; set; }
public List<AppUserDTO> Players { get; set; } = new();
}
When a User accepts an invitation he should be added to the List<AppUserDTO> Players List, I do this this way on the client side:
private async Task AcceptInvite()
{
Team.Players.Add(Player);
await TeamDataService.UpdateTeam(Team);
}
public async Task UpdateTeam(Team team)
{
var teamJson =
new StringContent(JsonSerializer.Serialize(team), Encoding.UTF8, "application/json");
await _httpClient.PutAsync("api/team", teamJson);
}
But I get the following exception on the server side when I'd like to save the changes to the server:
System.InvalidOperationException: The instance of entity type 'AppUserDTO' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
With the server-side code being:
public Team UpdateTeam(Team team)
{
var updatedTeam = _appDbContext.Teams.Include(t => t.Players).FirstOrDefault(t => t.TeamID == team.TeamID);
if (updatedTeam == null) return null;
updatedTeam.TeamID = team.TeamID;
updatedTeam.Name = team.Name;
updatedTeam.Abbreviation = team.Abbreviation;
updatedTeam.TownID = team.TownID;
updatedTeam.StatisticsID = team.StatisticsID;
updatedTeam.Players = team.Players;
updatedTeam.CaptainID = team.CaptainID;
_appDbContext.SaveChanges();
return updatedTeam;
}
The exception pops up at the _appDbContext.SaveChanges() method.
What I noticed is the following: When I add an Entity to an empty list and save it, I get no exception, but if the list already has Entities I get this error.
What would be the solution for this, I believe is quite common what I try to do, but I didn't find a solution anywhere for this.
When you execute:
var updatedTeam = _appDbContext.Teams
..Include(t => t.Players).FirstOrDefault(t => t.TeamID == team.TeamID);
... you are retrieving existing Players from the Db and _appDbContext is tracking them (by "ID").
Now, when you set Players:
updatedTeam.Players = team.Players;
... I suspect that team.Players includes Players that are already being tracked by the _appDbContext. Hence your error.
You could try:
List<Player> playersToAdd = team.Players.Except(updatedTeam.Players);
updatedTeam.AddRange(playersToAdd);
In this way, you are not adding duplicate players to the context that are already being tracked from the initial database retrieval.

Handling Dates with OData v4, EF6 and Web API v2.2

I'm in the midst of upgrading from v1-3 to v4, but I've run into a few problems.
My understanding is that DateTime is unsupported, and I have to always use DateTimeOffset. Fine.
But before I was storing Sql date data type in the DateTime, now it seems I get this error:
Member Mapping specified is not valid. The type 'Edm.DateTimeOffset[Nullable=False,DefaultValue=,Precision=]' of member 'CreatedDate' in type 'MyEntity' is not compatible with 'SqlServer.date[Nullable=False,DefaultValue=,Precision=0]'
What is the work around for this? I need to be able to store specifically just dates in the database (time and locality is not important). Would be great if I could get the Edm.Date aswell as a returned data type, but I didn't have that before.
Thanks.
Edit: Example classes
Before:
public class Ticket
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required, MaxLength(50)]
public string Reference { get; set; }
[Column(TypeName = "date")]
public DateTime LoggedDate { get; set; }
}
After:
public class Ticket
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required, MaxLength(50)]
public string Reference { get; set; }
[Column(TypeName = "date")]
public DateTimeOffset LoggedDate { get; set; }
}
This isn't valid in EF.
One option is to define a new property in the entity. Say Title is mapped to EF:
public partial class Title
{
public int Id { get; set; }
public string Name { get; set; }
public Nullable<System.DateTime> CreatedOn { get; set; }
}
then add a new property of DateTimeOffset:
public partial class Title
{
[NotMapped]
public DateTimeOffset? EdmCreatedOn
{
// Assume the CreateOn property stores UTC time.
get
{
return CreatedOn.HasValue ? new DateTimeOffset(CreatedOn.Value, TimeSpan.FromHours(0)) : (DateTimeOffset?)null;
}
set
{
CreatedOn = value.HasValue ? value.Value.UtcDateTime : (DateTime?)null;
}
}
}
and the code for generate OData Model looks like:
public static IEdmModel GetModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
EntityTypeConfiguration<Title> titleType= builder.EntityType<Title>();
titleType.Ignore(t => t.CreatedOn);
titleType.Property(t => t.EdmCreatedOn).Name = "CreatedOn";
builder.EntitySet<Title>("Titles");
builder.Namespace = typeof(Title).Namespace;
return builder.GetEdmModel();
}
}
The controller looks like:
public class TitlesController : ODataController
{
CustomerManagementSystemEntities entities = new CustomerManagementSystemEntities();
[EnableQuery(PageSize = 10, MaxExpansionDepth = 5)]
public IHttpActionResult Get()
{
IQueryable<Title> titles = entities.Titles;
return Ok(titles);
}
public IHttpActionResult Post(Title title)
{
entities.Titles.Add(title);
return Created(title);
}
}
For anyone coming to this in the future, the OData v4 team have fixed this issue.
[Column(TypeName = "date")]
public DateTime Birthday { get; set; }
This will now auto-resolve to Edm.Date.
If you are like me and are doing date type by convention, you have to manually declare the properties as dates lest they be auto-resolved as DateTimeOffset. OData currently does not allow you to add your own conventions.
customer.Property(c => c.Birthday).AsDate();
http://odata.github.io/WebApi/#12-01-DateAndTimeOfDayWithEF
You can refer to the link below to define your DateTimeAndDateTimeOffsetWrapper to do the translation between two types.
http://www.odata.org/blog/how-to-use-sql-spatial-data-with-wcf-odata-spatial/
Define two properties on your model, one is DateTime which only exists in the Edm model, the other is DateTimeOffset which only exists in the DB.
If the solution above doesn't meet your request, you have to change the data to DateTime before saving it to database and change it back to DateTimeOffset after retrieving it from database in the controller actions.
You can define two almost-same classes to achieve this. The only difference is that one has DateTime property and the other has DateTimeOffset property.
The former one is used for EF and mapping into DB.
The latter one is used for defining OData Edm model and presenting to the users.
As I said above, you have to do the translation between these two classes before saving the data and after retrieving the data.
You can add the AppendDatetimeOffset method to add automatically the methods
using the microsoft T4 engine (i.e. updating the template file *.tt). So that when regenerating the code, you don't have to append classes again. Hope this Helps :)
public string Property(EdmProperty edmProperty)
{
return string.Format(
CultureInfo.InvariantCulture,
(_ef.IsKey(edmProperty) ? "[Key]" : "") +
"{0} {1} {2} {{ {3}get; {4}set; }} {5}",
Accessibility.ForProperty(edmProperty),
_typeMapper.GetTypeName(edmProperty.TypeUsage),
_code.Escape(edmProperty),
_code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
_code.SpaceAfter(Accessibility.ForSetter(edmProperty)),
AppendDateTimeOffset(edmProperty));
}
public string AppendDateTimeOffset(EdmProperty edmProperty){
if(!_typeMapper.GetTypeName(edmProperty.TypeUsage).Contains("DateTime")) return " ";
//proceed only if date time
String paramNull = #"public Nullable<System.DateTimeOffset> edm{0}
{{
get
{{
return {0}.HasValue ? new DateTimeOffset({0}.Value, TimeSpan.FromHours(0)) : (DateTimeOffset?)null;
}}
}}";
String paramNotNull = #"public System.DateTimeOffset edm{0}
{{
get
{{
return new DateTimeOffset({0}, TimeSpan.FromHours(0));
}}
}}";
String s= String.Empty;
if(edmProperty.Nullable){
s = string.Format(paramNull, edmProperty.Name);
}else
{
s = string.Format(paramNotNull, edmProperty.Name);
}
return s;
}

How to achieve custom object materialization from DB using Entity Framework

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.

Using Entity Framework 4.0 with Code-First and POCO: How to Get Parent Object with All its Children?

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;
}