In a project that involves an Asp.Net Core API and Entity Framework, I have the following entities:
public class A
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; private set; }
[Required] public string Name { get; set; }
[Required] public B MyObj { get; set; }
}
public class B
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; private set; }
}
My API has this model, which represents a POST request body for the creation of a new A:
public class A_Post
{
public string Name { get; set; }
public string Obj_Id { get; set; }
}
Here, Obj_Id refers to the Id of the B instance that I want A.MyObj to have as its value.
What I want to do is add a method to A that creates an instance of A from an instance of A_Post. Something like this:
public static A CreateFromPost(A_Post post)
{
var entity = new A();
entity.Name = post.Name;
// post.Obj_Id is accessible here, but cannot set entity.MyObj
return entity;
}
However, I'm not sure how to go from having the Obj_Id to having the entity it corresponds to, without getting it from the context. Is there a way to do this? Or will I have to set MyObj somewhere else in the program, where the context can be accessed?
Related
My database has two tables - RuleGroups and Rules. My Entity Framework classes are the following:
public class RuleGroup
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<Rule> Rules { get; set; }
}
public class Rule
{
[Key]
public Guid Id { get; set; }
public Guid RuleGroupId { get; set; }
public string Name { get; set; }
public ICollection<Condition> Conditions { get; set; }
[ForeignKey("RuleGroupId")]
public virtual RuleGroup RuleGroup { get; set; }
}
[NotMapped]
public class Condition
{
public Guid Id { get; set; }
public string Name { get; set; }
}
Class Condition is not mapped because it is being serialized and stored as JSON in Rule Table (using this example)
My DTOS are the following:
public class UpdateRuleGroupDto
{
public string Name { get; set; }
public ICollection<UpdateRuleDto> Rules { get; set; }
}
public class UpdateRuleDto
{
public string Name { get; set; }
public ICollection<UpdateConditionDto> Conditions { get; set; }
}
public class UpdateConditionDto
{
public string Name { get; set; }
}
In my Startup.cs I initialize Automapper :
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<UpdateRuleGroupDto, RuleGroup>();
cfg.CreateMap<UpdateRuleDto, Rule>();
cfg.CreateMap<UpdateConditionDto, Condition>();
}
I have an API controller endpoint that accepts JSON PATCH document to make changes to data stored in database.
public IActionResult Patch(Guid ruleGroupId, [FromBody]JsonPatchDocument<UpdateRuleGroupDto> body)
{
RuleGroup ruleGroupFromRepo = _deviceRules.GetRuleGroup(ruleGroupId);
UpdateRuleGroupDto ruleGroupToPatch = Mapper.Map<UpdateRuleGroupDto>(ruleGroupFromRepo);
// Patching logic here
Mapper.Map(ruleGroupToPatch, ruleGroupFromRepo);
context.SaveChanges();
return NoContent();
}
The problem:
When changes are made/saved, Rules in Rule table change their/get new GUID.
Example, say we have this data in 2 Tables.
RuleGroup Table
[Id][Name]
[ddad5cac-e5a1-4db7-8167-66a6de3b8a0c][Test]
Rule Table
[Id][RuleGroupId][Name][Condition]
[17c38ee8-4158-4ecc-b893-97786fa76e13][ddad5cac-e5a1-4db7-8167-66a6de3b8a0c][Test][[{"Name":"Test"}]]
If I change field [Name] to a new value, Rules Table will look like this.
Rule Table
[Id][RuleGroupId][Name][Condition]
[ba106de8-bcbc-4170-ba56-80fe619cd757][ddad5cac-e5a1-4db7-8167-66a6de3b8a0c][Test2][[{"Name":"Test"}]]
Note that [Id] field has now a new GUID.
EDIT
#Gert Arnold made me realize that I'm not attaching entities.
I ran the following code:
foreach (var item in ruleGroupFromRepo.rules)
{
var x = _context.Entry(item).State;
}
and all the states were Added and not modified. Now I just have to figure out how to do it properly.
I'm using Entity Framework 6.0 and MVC and I have one of the following model:
public class Person
{
[Key]
public int Id { get; set; }
public String Name { get; set; }
public String SocialNumber {get;set;}
}
public class Organization
{
[Key]
public Guid? OrgId { get; set; }
public List<Person> Members { get; set; }
}
I'm getting this data by doing something like
bool hideData = false;
var sResult = DBContext.Organization.FirstOrDefault(s => s.OrgId == inputID)
Is there an elegant way for me to set the SocialNumber = "######" when hideData is true and show the value when it is false?
Before you tell me that I can just loop through the object and set them explicitly, the reason I don't want to do this because I simpled down the above example, in reality, my class is buried deep in many many objects.
I have two entities (student, yogaspaceevent). Yogaspaceevent which is like a exercise class, has a collection of students as RegisteredStudents and I want to be able to add students to this event to register them.
I tried a couple of things but can't seem to add a student to a yogaspaceevent, so maybe I'm not doing it correctly.
Question: do I need to create a whole new entity (say registeredstudents) to add students to a yogaspaceevent? Because I thought Entity Framework would somehow create this table for me or do some other magic under the hood to add a student to a event? The only solution I have is to create a RegisteredStudent entity and then EF will create a table. Is this the right solution?
Here are my two entities:
public class YogaSpaceEvent
{
public int YogaSpaceEventId { get; set; }
[Index]
public DateTime DateTimeScheduled { get; set; }
public int AppointmentLength { get; set; }
public int StatusEnum { get; set; }
public virtual ICollection<Student.Student> RegisteredStudents { get; set; }
[Index]
public int YogaSpaceRefId { get; set; }
[ForeignKey("YogaSpaceRefId")]
public virtual YogaSpace YogaSpace { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public virtual StudentImage StudentImage { get; set; } //one-to-one
[MaxLength(50)]
public string CatchPhrase { get; set; }
[MaxLength(250)]
public string Education { get; set; } //Serra High School, San Diego State Univesity
public string Work { get; set; } // Taco Bell, Starbucks
[MaxLength(250)]
public string WhyIPractice { get; set; }
public StudentStatus Status { get; set; }
}
Here is my controller where I want to add a student to an event but I get exceptions thrown.
[HttpPost]
public ActionResult RegisterStudentForEvent(int eventId)
{
try
{
Student student = _studentRepository.Find(User.Identity.GetUserId()); //User.Identity.GetUserId()
if (student == null)
{
return Json( new { error = "notification", message = "You need an active student profile to register, please complete your student profile." });
}
var spaceEvent = _yogaSpaceEventRepository.Find(eventId);
if (spaceEvent.RegisteredStudents == null)
{
spaceEvent.RegisteredStudents = new Collection<Student>(); // new EntityCollection<Student>(); neither of these two work, both throw exceptions
}
spaceEvent.RegisteredStudents.Add(student);
_yogaSpaceEventRepository.Modify(spaceEvent);
_yogaSpaceEventRepository.Save();
// send email confirmation with a cancel link in it.
return Json(new { error = "false", message = "Now registered!. A confirmation email has been sent. Have fun! " });
}
catch (Exception ex)
{
return Json( new { error = "true", message = ex.Message });
}
}
Here are the two exceptions that are thrown when I try both methods above.
The object could not be added to the EntityCollection or EntityReference. An object that is attached to an ObjectContext cannot be added to an EntityCollection or EntityReference that is not associated with a source object.
and
The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.
I was having similar issue.
Your above code seems to be correct , I think you have missed to specify the mapping in dbcontext.cs class which resulted in below error:
The object could not be added to the EntityCollection or
EntityReference. An object that is attached to an ObjectContext cannot
be added to an EntityCollection or EntityReference that is not
associated with a source object.
I hope my below example could help you.
Specialization (Parent Entity Class)
[Table("M_Specialization")]
public partial class Specialization : Entity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public int SID { get; set; }
public string SCode { get; set; }
public string Description { get; set; }
public virtual ICollection<SubSpecialization> SubSpecializationDetails { get; set; }
}
Sub specialization (Child Entity Class)
[Table("M_SubSpecialization")]
public partial class SubSpecialization : Entity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Int32 SuID { get; set; }
public string SubCode { get; set; }
public Int32 SID { get; set; } // This is required in order to map between specialization and subspecialization
[ForeignKey("SID")]
public virtual Specialization Specialization { get; set; }
}
dbcontext.cs:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<SubSpecialization>().HasRequired(c => c.Specialization)
.WithMany(s => s.SubSpecializationDetails)
.HasForeignKey(c => c.SID);
}
You need to define a many-to-many relationship between Student and YogaSpaceEvent. The code you have created so far represents a one-to-many relationship.
An description of how to do that can be found here and here
What you need to add to your code is:
- Add a ICollection property to your Student class
- In your dbcontext.OnModelCreating, add code to define the many-to-many relationship.
As far as I can see, your controller function is OK.
With Entity Framework, if I have the following model:
class Asset {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<AssetGroup> Groups { get; set; }
}
class AssetGroup {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Asset> Assets { get; set; }
}
Adding an existing asset to a given group without having to query the database to load the asset in question is relatively straightforward:
using(var context = new MyContext()) {
AssetGroup assetGroup = context.AssetGroups.Find(groupId);
// Create a fake Asset to avoid a db query
Asset temp = context.Assets.Create();
temp.Id = assetId;
context.Assets.Attach(temp);
assetGroup.Assets.Add(temp);
context.SaveChanges();
}
However, I now have the problem that I have extended the model so that there are multiple Asset types, implemented as an inheritance hierarchy. In the process, the Asset class has been made abstract, since an asset with no specific type makes no sense:
abstract class Asset {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<AssetGroup> Groups { get; set; }
}
class SpecificAsset1 : Asset {
public string OneProperty { get; set; }
}
class SpecificAsset2 : Asset {
public string OtherProperty { get; set; }
}
The problem is that now the Create() call throws an InvalidOperationException because "Instances of abstract classes cannot be created", which of course makes sense.
So, the question is, how can I update the many-to-many relation without having to go fetch the Asset from the database?
You can use the generic Create<T> method of DbSet<T> to create a derived entity object.
var temp1 = context.Assets.Create<SpecificAsset1>();
and continue from here using temp1 as a stub entity the way you already did.
I'm new to Entity Framework, in the past I've used Enterprise Library or ADO.NET directly to map models to database tables. One pattern that I've used is to put my common audit fields that appear in every table in a Base Class and then inherit that Base Class for every object.
I take two steps to protect two of the fields (Created, CreatedBy):
Have a parameterless constructor private on the Base Enitity and create a second one that requires Created and CreatedBy are passed on creation.
Make the setters Private so that the values cannot be changed after the object is created.
Base Class:
using System;
namespace App.Model
{
[Serializable()]
public abstract class BaseEntity
{
public bool IsActive { get; private set; }
public DateTimeOffset Created { get; private set; }
public string CreatedBy { get; private set; }
public DateTimeOffset LastUpdated { get; protected set; }
public string LastUpdatedBy { get; protected set; }
private BaseEntity() { }
protected BaseEntity(DateTimeOffset created, string createdBy)
{
IsActive = true;
Created = created;
CreatedBy = createdBy;
LastUpdated = created;
LastUpdatedBy = createdBy;
}
}
}
Inherited Class:
using System;
namespace App.Model
{
[Serializable()]
public class Person : BaseEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public Person(DateTimeOffset created, string createdBy) :
base(created, createdBy) { }
}
}
I've run into issues with both. EF requires a parameterless constructor to create objects. EF will not create the database columns that have a private setter.
My question is if there is a better approach to accomplish my goals with EF:
Require that the values for Created and CreatedBy are populated at instantiation.
The values of Created and CreatedBy cannot be changed.
You could instantiate a context with a constructor that accepts a string createdBy. Then in an override of SaveChanges():
public override int SaveChanges()
{
foreach( var entity in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added)
.Select (e => e.Entity)
.OfType<BaseEntity>())
{
entity.SetAuditValues(DateTimeOffset.Now, this.CreatedBy);
}
return base.SaveChanges();
}
With SetAuditValues() as
internal void SetAuditValues(DateTimeOffset created, string createdBy)
{
if (this.Created == DateTimeOffset.MinValue) this.Created = created;
if (string.IsNullOrEmpty(this.CreatedBy)) this.CreatedBy = createdBy;
}
After the entities have been materialized from the database the values won't be overwritten when someone calls SetAuditValues.
You shouldn't be trying to control access rights directly on your entities/data layer instead your should do this in your application layer. This way you have a finer level of control over what users can do what.
Also rather than have the audit fields repeated on every table you might want to store your Audit records in another table. This is easy to do with code first:
public class AuitRecord
{
public bool IsActive { get; set; }
public DateTimeOffset Created { get; set; }
public string CreatedBy { get; set; }
public DateTimeOffset LastUpdated { get; set; }
public string LastUpdatedBy { get; set; }
}
You would then link the base class with the audit record to it:
public abstract class BaseEntity
{
public AuditRecord Audit { get; set; }
}
And finally your actually entities
public class Person : BaseEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
You can no access the audit data by going:
Person.Audit.IsActive