I have these entities.
[Table("ServiceTickets", Schema = "dbo")]
public class ServiceTicket
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime CreationDateTime { get; set; } = DateTime.Now;
[Column(TypeName = "varchar(1000)")]
public string Issue { get; set; } = string.Empty;
public DateTime? ReportedDateTime { get; set; }
public DateTime? ResolutionDateTime { get; set; }
public int CreatedBy { get; set; }
public int AttendedBy { get; set; }
public int ConfirmedBy { get; set; }
public int SrNumber { get; set; }
public int StatusId { get; set; }
public ServiceTicketSubInformation? ServiceTicketSubInformation { get; set; }
public virtual StatusEnum? StatusEnums { get; set; }
public virtual ICollection<WorkOrder>? WorkOrders { get; set; }
}
[Table("ServiceTicketSubInfo", Schema = "dbo")]
public class ServiceTicketSubInformation
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int LocationId { get; set; }
public int SubLocationId { get; set; }
public int ReporterId { get; set; }
}
[Table("WorkOrder", Schema = "dbo")]
public class WorkOrder
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime? CreationDateTime { get; set; }
[Column(TypeName = "varchar(1000)")]
public string ActionTaken { get; set; } = string.Empty;
public DateTime? WorkStartTime { get; set; }
public DateTime? WorkCompleteTime { get; set; }
public int SrNumber { get; set; }
public int Status { get; set; }
}
[Table("Statuses", Schema = "dbo")]
public class StatusEnum
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column(TypeName = "varchar(50)")]
public string Name { get; set; } = string.Empty;
}
It builds successfully, I need to insert a new record. So using Swagger, I change the values to this:
{
"id": 0,
"creationDateTime": "2022-07-24T12:41:49.666Z",
"issue": "My Issue",
"reportedDateTime": "2022-07-24T12:41:49.666Z",
"resolutionDateTime": "2022-07-24T12:41:49.666Z",
"createdBy": 1,
"attendedBy": 1,
"confirmedBy": 1,
"srNumber": 1,
"statusId": 1,
"serviceTicketSubInformation": {
"id": 1,
"locationId": 1,
"subLocationId": 1,
"reporterId": 1
},
"statusEnums": {
"id": 1,
"name": "Completed"
},
"workOrders": [
{
"id": 1,
"creationDateTime": "2022-07-24T12:41:49.666Z",
"actionTaken": "My Action Taken",
"workStartTime": "2022-07-24T12:41:49.666Z",
"workCompleteTime": "2022-07-24T12:41:49.666Z",
"srNumber": 1,
"status": 1
}
]
}
When I try to add
public async Task<ServiceTicket> UpsertServiceTicket(ServiceTicket model)
{
await context.ServiceTickets.AddAsync(model);
var result = await context.SaveChangesAsync();
model.Id = result;
return model;
}
I get this error:
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert explicit value for identity column in table 'ServiceTicketSubInfo' when IDENTITY_INSERT is set to OFF.
at Microsoft.Data.SqlClient.SqlCommand.<>c.b__188_0(Task`1 result)
What am I doing wrong here?
The issue is that your API is accepting models that look like Entities but aren't actually entities. This can work where you have an entity graph that is solely parent-child relationships, but can fall down the minute you introduce many-to-one references.
Think of it this way, if your API accepted a DTO object for the relationships, what would it look like and how would you process it?
The DTO would contain the details to create a new ServiceTicket, along with the detail to know which "SubInformation" applies to it. In your case you want to associate it with a SubInformation ID of #1.
This is different to a case where we want to Create a SubInformation. The issue is that while the ServiceTicket passed in contains something that looks like an existing SubInformation, it isn't an existing SubInformation as far as the DbContext servicing that request is concerned. It is a deserialized blob of JSON that the DbContext will treat as a new entity, and having an ID value means you're either going to get a duplicate exception, or EF would ignore the ID all-together and insert a new one with the next available ID, such as #13.
To address this, the DbContext needs to know that SubInformation #1 is referencing a Tracked, known entity:
A) The Hack:
public async Task<ServiceTicket> InsertServiceTicket(ServiceTicket model)
{
context.Attach(model.SubInformation);
await context.ServiceTickets.AddAsync(model);
var result = await context.SaveChangesAsync();
model.Id = result;
return model;
}
This assumes that you trust that the details in the SubInformation actually point to a valid row in the DB. If it doesn't then this operation will fail. This is also a common hack for when APIs/Controllers accept entities to be updated:
public async Task<ServiceTicket> UpdateServiceTicket(ServiceTicket model)
{
context.Attach(model);
context.Entry(model).State = EntityState.Modified;
var result = await context.SaveChangesAsync();
return model;
}
When attaching entities, one of the worst things you can do is set the EntityState to Modified in order to update all of the values. The issue is that you are explicitly trusting all of the values passed, which is extremely dangerous since it exposes your system to tampering in the request data.
B) Better:
public async Task<ServiceTicket> InsertServiceTicket(ServiceTicket model)
{
var subInformation = context.SubInformations.Single(x => x.Id == model.serviceTicketSubInformation.Id);
model.serviceTicketSubInformation = subInformation;
// Repeat for any and all other references... I.e. WorkOrders
await context.ServiceTickets.AddAsync(model);
var result = await context.SaveChangesAsync();
model.Id = result;
return model;
}
This solution queries for valid entities based on the IDs passed in and replaces the references with tracked entities. This way when we insert our service ticket, all references are validated and tracked. If we specify a SubInformation ID that doesn't exist, it will still throw an exception but the call stack will make it clear exactly what reference wasn't found rather than some error on SaveChanges. The issue with this approach is that it's easy to forget a reference, leading to repeated errors popping up as the system evolves.
C) Best, DTOs:
Using DTOs we can condense our payload for an Insert down to:
{
"id": 0,
"creationDateTime": "2022-07-24T12:41:49.666Z",
"issue": "My Issue",
"reportedDateTime": "2022-07-24T12:41:49.666Z",
"resolutionDateTime": "2022-07-24T12:41:49.666Z",
"createdBy": 1,
"attendedBy": 1,
"confirmedBy": 1,
"srNumber": 1,
"statusId": 1,
"serviceTicketSubInformationId": 1,
"workOrdersIds": [ 1 ],
}
When we go to insert our service ticket:
public async Task<ServiceTicketSummaryDTO> InsertServiceTicket(NewServiceTicketDTO model)
{
var subInformation = context.SubInformations.Single(x => x.Id == model.ServiceTicketSubInformationId);
var workOrders = await context.WorkOrders
.Where(x => model.WorkOrderIds.Contains(x.Id))
.ToListAsync();
if (workOrders.Count != model.WorkOrderIds.Count)
throw new ArgumentException("One or more work orders were not valid.");
var serviceTicket = _mapper.Map<ServiceTicket>(model);
serviceTicket.SubInformation = subInformation;
serviceTicket.WorkOrders = workOrders;
await context.ServiceTickets.AddAsync(serviceTicket);
await context.SaveChangesAsync();
return _mapper.Map<ServiceTicketSummaryDTO>(serviceTicket);
}
Here we accept a DTO for the details of a new Service Ticket. This DTO only needs to contain the IDs for any references which the call will validate and resolve before populating a new entity and passing back a DTO with details that the consumer will be interested in. (Either built from the entity we just created or querying the DBContext if there is more info needed.) This has the added benefit of cutting down the size of data being passed back and forth and ensures only data we expect to be altered is accepted.
Related
I am assigned the implementation of a REST GET with a complex DB model and somewhat complex output layout. Although I am a REST beginner, I have lost "rest" on this for 2 weeks spinning my wheels, and Google was of no help as well.
Here's a simplification of the existing DB I am given to work with:
Table group : {
Column id Guid
Column name string
Primary key: {id}
}
Table account
{
Column id Guid
Column name string
Primary key: {id}
}
Table groupGroupMembership
{
Column parentGroupId Guid
Column childGroupId Guid
Primary key: {parentGroupId, childGroupId}
}
Table accountGroupMembership
{
Column parentGroupId Guid
Column childAccountId Guid
Primary key: {parentGroupId, childAccountId}
}
So clearly you guessed it: There is a many-to-many relationship between parent a child groups. Hence a group can have many parent and child groups. Similarly, an account can have many parent groups.
The DB model I came up with in C# (in namespace DBAccess.Models.Tables):
public class Group
{
// properties
public Guid id { get; set; }
public string? name { get; set; }
// navigation properties
public List<GroupMemberAccount>? childAccounts { get; set; }
public List<GroupMemberGroup>? childGroups { get; set; }
public List<GroupMemberGroup>? parentGroups { get; set; }
}
public class Account
{
// properties
public Guid id { get; set; }
public string? name { get; set; }
// navigation properties
public List<GroupMemberAccount>? parentGroups { get; set; }
}
public class GroupMemberAccount
{
// properties
public Guid parentGroupId { get; set; }
public Guid childAccountId { get; set; }
//navigation properties
public Group? parentGroup { get; set; }
public Account? childAccount { get; set; }
static internal void OnModelCreating( EntityTypeBuilder<GroupMemberAccount> modelBuilder )
{
modelBuilder.HasKey(gma => new { gma.parentGroupId, gma.childAccountId });
modelBuilder
.HasOne(gma => gma.parentGroup)
.WithMany(g => g.childAccounts)
.HasForeignKey(gma => gma.parentGroupId);
modelBuilder
.HasOne(gma => gma.childAccount)
.WithMany(a => a.parentGroups)
.HasForeignKey(gma => gma.childAccountId);
}
}
public class GroupMemberGroup
{
// properties
public Guid parentGroupId { get; set; }
public Guid childGroupId { get; set; }
//navigation properties
public Group? parentGroup { get; set; }
public Group? childGroup { get; set; }
static internal void OnModelCreating(EntityTypeBuilder<GroupMemberGroup> modelBuilder)
{
modelBuilder.HasKey(gmg => new { gmg.parentGroupId, gmg.childGroupId });
modelBuilder
.HasOne(gmg => gmg.parentGroup)
.WithMany(g => g.childGroups)
.HasForeignKey(gmg => gmg.parentGroupId);
modelBuilder
.HasOne(gmg => gmg.childGroup)
.WithMany(g => g.parentGroups)
.HasForeignKey(gmg => gmg.childGroupId);
}
}
The corresponding DTO model I created:
public class Account
{
public Guid id { get; set; }
public string? name { get; set; }
public List<GroupMemberAccount>? parentGroups { get; set; }
}
public class AccountMappingProfile : AutoMapper.Profile
{
public AccountMappingProfile()
{
CreateMap<DBAccess.Models.Tables.Account, Account>();
}
}
public class Group
{
public Guid id { get; set; }
public string? Name { get; set; }
public GroupChildren children { get; set; } = null!;
};
public class GroupChildren
{
public List<GroupMemberAccount>? childAccounts { get; set; } = null!;
public List<GroupMemberGroup>? childGroups { get; set; } = null!;
}
public class GroupMemberAccount
{
public Guid parentGroupId { get; set; }
public Guid childAccountId { get; set; }
//public Group? parentgroup { get; set; } // commented out because no need to output in a GET request
public Account? childAccount { get; set; }
}
public class GroupMemberGroup
{
public Guid parentGroupid { get; set; }
public Guid childGroupId { get; set; }
//public Group? parentGroup { get; set; }; // commented out because no need to output in a GET request
public Group? childGroup { get; set; };
}
What you need to spot here is the difference in classes Group between the DB and DTO models.
In the DB model, Group has 3 lists: childAccounts, childGroups and parentGroups.
In the DTO model, Group has 1 node children of type GroupChildren which is a class that contains 2 of those 3 lists.
Hence an additional difficulty when it comes to design the mapping. That difference is intentional because it matches the following desired output for an endpoint such as: GET .../api/rest/group({some group guid}) is something like:
{
"id": "some group guid",
"name": "some group name",
"children": {
"childAccounts":{
"account":{ "name": "some account name 1"}
"account":{ "name": "some account name 2"}
...
}
"childFroups":{
"group":{ "name": "some group name 1"}
"group":{ "name": "some group name 2"}
...
}
},
}
obtained from following typical controller code:
[HttpGet("Groups({key})")]
[ApiConventionMethod(typeof(ApiConventions),
nameof(ApiConventions.GetWithKey))]
public async Task<ActionResult<Group>> Get(Guid key, ODataQueryOptions<Group> options)
{
var g = await (await context.Group.Include(g => g.childAccounts)
.Include(g => g.childGroups)
.Where(g => g.id == key)
.GetQueryAsync(mapper, options) // note the mapper here is the mapping defined below
).FirstOrDefaultAsync();
if (g is null)
{
return ResourceNotFound();
}
return Ok(g);
}
So here's the missing part to all this. Unless there are major errors in all of the above, I have a very strong intuition that it is the mapping that is failing to get me the requested output above.
public class GroupMappingProfile : AutoMapper.Profile
{
public GroupMappingProfile()
{
// the rather straightforward.
CreateMap<DBAccess.Models.Tables.GroupMemberAccount, GroupMemberAccount>();
CreateMap<DBAccess.Models.Tables.GroupMemberGroup, GroupMemberGroup>();
//Attempt 1: the not so straightforward. An explicit exhaustive mapping of everything, down to every single primitive type
CreateMap<DBAccess.Models.Tables.Group, Group>()
.ForMember(g => g.children, opts => opts.MapFrom(src => new GroupMembers
{
childAccounts = src.childAccounts!.Select(x => new GroupMemberAccount { parentGroupId = x.parentGroupId,
childAccountId = x.childAccountId,
childAccount = new Account { id = x.childAccount!.id,
name = x.childAccount!.name
}
}
).ToList(),
//childGroups = src.childGroups!.Select(x => new GroupMemberGroup(x)).ToList(),
childGroups = src.childGroups!.Select(x => new GroupMemberGroup { parentGroupId = x.parentGroupId,
childGroupId = x.childGroupId,
childGroup = new Group { id = x.childGroup!.id,
name = x.childGroup!.name
}
}
).ToList(),
}));
//Attempt 2: mapper injection
IMapper mapper = null!;
CreateMap<DBAccess.Models.Tables.Group, Group>()
.BeforeMap((_, _, context) => mapper = (IMapper)context.Items["mapper"]) //ADDING THIS LINE CAUSES ALL QUERIES TO LOOK FOR A NON EXISTENT Group.Groupid column
.ForMember(g => g.children, opts => opts.MapFrom(src => new GroupMembers
{
childAccounts = mapper.Map<List<DBAccess.Models.Tables.GroupMemberAccount>, List<GroupMemberAccount>>(src.childAccounts!),
childGroups = mapper.Map<List<DBAccess.Models.Tables.GroupMemberGroup>, List<GroupMemberGroup>>(src.childGroups!)
}))
}
}
Attempt1 will yield:
{
"id": "some guid",
"name": "some name"
"children": {}
}
even though the generated SQL does fetch all the required data to fill "children"
Attempt2 (mapper injection) is a technique I was suggested and have no clue how it is supposed to work. From what I gather, the mapping functions creates a few maps for some basic types while it uses its "future" self to create the remaining mappings, whenever it will be invoked in the future. Looks somehow like a one-time recursion.
However, it crashes as the generated SQL will look for a non-existent view column group.Groupid
SELECT [t].[id], [t].[name],
[g0].[parentGroupId], [g0].[childAccountId],
[g1].[parentGroupId], [g1].[childGroupId], [g1].[Groupid] -- where does [g1].[Groupid] come from??
FROM (
SELECT TOP(1) [g].[id], [g].[name]
FROM [HID_Rest].[group] AS [g]
WHERE [g].[id] = #__key_0
) AS [t]
LEFT JOIN [HID_Rest].[groupMemberAccount] AS [g0] ON [t].[id] = [g0].[parentGroupId]
LEFT JOIN [HID_Rest].[groupMemberGroup] AS [g1] ON [t].[id] = [g1].[parentGroupId]
ORDER BY ...
So regardless of the mapping profile I experimented with, what is the right mapping profile I need (and what ever else) to get the expected JSON output above? Or is this desired JSON structure possible at all?
After further work, I have figured that there was nothing wrong with my models and mapping. There's still something wrong though as the output to my GET requests is still incomplete. Here's the current new issue I need to deal with to solve this problem:
Issue with REST controller function Microsoft.AspNetCore.Mvc.ControllerBase.OK()?
Hello I am new to servers and REST API and am trying to extract data from a dynamically created table and the data does not sync with the data in the database.
I have an sql database from which I extracted an entity database in asp.net web project.
This is an example for GET of one entity class (exists in database):
public class EmployeeBL
{
private FSProject1Entities db = new FSProject1Entities();
public List<Employee> GetEmployees(string fname, string lname, string depID)
{
return GetEmployeeSearchResult(fname, lname, depID);
}
}
And this is an example for a method from a class such as I created in order to combine data from 2 tables:
public class ShiftEmployeeDataBL
{
private FSProject1Entities db = new FSProject1Entities();
private List<ShiftEmployeeDataBL> GetEmployeeByShiftID(int id)
{
List<ShiftEmployeeDataBL> shiftEmpData = new List<ShiftEmployeeDataBL>();
foreach (Employee emp in db.Employee)
{//build list... }
return shiftEmpData;
}
My problem is that db.Employee via this GET request path (ShiftEmployeeData) is old data and via Employee GET request is good data (assuming the data was updated via Employee path).
And vice versa - it would appear that if I update Employee via ShiftEmployeeData class, it would appear as good data for ShiftEmployeeData class and not update for Employee.
I have APIcontrollers for both classes.
what is happening? I feel like I am missing something.
I tried closing cache options in browser.
update with code for elaboration:
entity Employee:
public partial class Employee
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int StartWorkYear { get; set; }
public int DepartmentID { get; set; }
}
employee update(auto generated by entity model code generation from db):
public void UpdateEmployee(int id, Employee employee)
{
Employee emp= db.Employee.Where(x => x.ID == id).First();
emp.FirstName = employee.FirstName;
emp.LastName = employee.LastName;
emp.StartWorkYear = employee.StartWorkYear;
emp.DepartmentID = employee.DepartmentID;
db.SaveChanges();
}
employeeshiftdata class (not a db table but still in the models folder):
public class EmployeeShiftData
{
public int ID { get; set; } //EmployeeID
public string FirstName { get; set; }
public string LastName { get; set; }
public int StartWorkYear { get; set; }
public string DepartmentName { get; set; }
public List<Shift> Shifts { get; set; }
}
employeeshift GET part of the controller:
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class EmployeeShiftDataController : ApiController
{
private static EmployeeShiftDataBL empShiftDataBL = new EmployeeShiftDataBL();
// GET: api/EmployeeShiftData
public IEnumerable<EmployeeShiftData> Get(string FirstName = "", string LastName = "", string Department = "")
{
return empShiftDataBL.GetAllEmployeeShiftData(FirstName, LastName, Department);
}
//...
}
Would need to see the code that interacts with the database, especially the code that makes the updates.
If the changes are written with Entity Framework, are the models themselves properly related with navigational properties?
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public List<EmployeeShift> EmployeeShifts { get; set; }
// etc.
}
public class EmployeeShift
{
public int Id { get; set; }
public Int EmployeeID { get; set; }
public Employee Employee { get; set; }
// etc.
}
If those are good, and both models are covered by Entity Framework's context tracking, then both should be updated.
I've got a simple "ContactsList" ASP.Net Core Web (REST) application, .Net Core 3.0, an MSSQL LocalDB, using MSVS 2019.
My "Contact" entity contains a list of "Notes".
When I create a new contact that already contains one or more notes, everything works fine. EF automagically inserts the notes into the notes table.
But when I try to UPDATE a contact, EF seems to disregard "notes".
Q: For "Updates", do I need write code in my controller to explicitly update the notes myself?
Or am I doing something "wrong", such that EF can't "automagically" do the updates it's supposed to?
Models/Contact.cs:
public class Contact
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ContactId { get; set; }
public string Name { get; set; }
public string EMail { get; set; }
public string Phone1 { get; set; }
public string Phone2 { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public List<Note> Notes { get; set; }
}
Models/Note.cs:
public class Note
{
public Note()
{
this.Date = DateTime.Now; // Default value: local "now"
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int NoteId { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
[ForeignKey("Contact")]
public int ContactId { get; set; }
}
Controllers/ContactsController.cs (POST works: if there are notes in the contacts list, it adds them):
[HttpPost]
public async Task<ActionResult<Contact>> PostContact(Contact contact)
{
_context.Contacts.Add(contact);
await _context.SaveChangesAsync();
//return CreatedAtAction("GetContact", new { id = contact.ContactId }, contact);
return CreatedAtAction(nameof(GetContact), new { id = contact.ContactId }, contact);
}
Controllers/ContactsController.cs (PUT seems to completely disregard any assocated notes):
[HttpPut("{id}")]
public async Task<IActionResult> PutContact(int id, Contact contact)
{
if (id != contact.ContactId)
{
return BadRequest();
}
_context.Entry(contact).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ContactExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
The SQL for POST shows four separate INSERTs: one for the contact, and one for each note.
The SQL for PUT only shows one UPDATE: just the contact; nothing else.
The debugger shows "notes" are clearly part of the "Contact" record that the controller received by PutContact().
Q: Should EF deal with "updating" notes automagically, or do I need to hand-code my updates in the controller?
Entity Framework Core ignores relationships unless you explicitly include them in queries.
_context.Entry(contact).State = EntityState.Modified;
The problem with the line above is that you did not specify that the related data has been modified, so it will be ignored in the query.
So you can either
attach all the related data
set the state of the related data to EntityState.Modified
or you can
query the object in the database and include the related data
and then assign the contact object to that queried object
var dbContactObj = _context.Contacts.Include(x => x.Notes).First(x => x.Id == contact.Id);
dbContactObj = contact;
_context.SaveChangesAsync();
Scenario: Complaint object can have many Votes. In the GET request for All Complaints, I want to return the Vote Count, and not each individual vote object with the Complaint in the API response.
Here's the main classes:
//Model: Complaint.cs
public class Complaint
{
public int Id { get; set; }
public string Summary { get; set; }
public List<Vote> Votes { get; set; }
public int UpVoteCount=> Votes.Count(v => v.IsUpvote);
public ApplicationUser Creator { get; set; }
}
//Model: Vote.cs
public class Vote
{
public int Id { get; set; }
public bool IsUpVote{ get; set; }
public Complaint Complaint { get; set; }
public ApplicationUser Creator { get; set; }
}
//DbContext: AppDbContext.cs
....
public IQueryable<Complaint> ComplaintsWithData =>
Complaint
.Include(complaint => complaint.Votes)
.AsNoTracking();
//ApiController: ComplaintsController.cs
[HttpGet]
public IEnumerable<Complaint> GetComplaints()
{
return _context.ComplaintsWithData.ToList();
}
In the current JSON response, I get the Vote count, however I also get each individual vote object's details as well (which I don't need in this call).
Current Response:
{
"id": 2,
"summary": "House was Stolen",
"votes": [
{
"id": 146,
"isUpvote": false,
"creator": null
},
{
"id": 147,
"isUpvote": false,
"creator": null
},
....
....
....
],
"upVoteCount": 211,
}
Desired Response:
{
"id": 2,
"summary": "House was Stolen",
"upVoteCount": 211,
}
I need to have the .Include(complaint => complaint.Votes) in the AppDbContext.cs file so that I can actually load the Votes to determine the Vote Count.
I do not want to store the Vote count as an actual database column.
Any advice would be greatly appreciated!
I am using .NET Core 2.0 Web API with Entity Framework Core.
Thanks in advance.
You can use OptIn on your class for members serialization:
//..
using Newtonsoft.Json;
//..
[JsonObject(MemberSerialization.OptIn)]
public class Complaint
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("summary")]
public string Summary { get; set; }
public List<Vote> Votes { get; set; }
[JsonProperty("upVoteCount")]
public int UpVoteCount=> Votes.Count(v => v.IsUpvote);
public ApplicationUser Creator { get; set; }
}
Maybe something like:
public List<Complaint> ComplaintsWithData()
{
return this.DbContext.Complaints
.Include(complaint => complaint.Votes)
.Select(p => new Complaint
{
Id = p.Id,
IsUpVote = p.IsUpVote,
UpVoteCount = p.Votes.Count
}).ToList();
}
There are several options. Below are two of them:
You might consider adding view models - classes with the exact set of properties you would like to return to UI. You just need to map your entity records to them.
Or just mark the properties you'd like to hide from being serialized to your JSON or XML endpoint response with NonSerialized attribute.
public class Complaint
{
public int Id { get; set; }
public string Summary { get; set; }
[NonSerialized]
public List<Vote> Votes { get; set; }
public int UpVoteCount=> Votes.Count(v => v.IsUpvote);
public ApplicationUser Creator { get; set; }
}
I am using Entity Framework Core following Chris Sakell's blog here.
He uses generics to manage his repositories and also a base repository that he uses for all the other repositories.
Part of the base repository has the the following code for the retrieval of a single entity that also downloads related entities using the includeProperties option. Here is the generic code for a retrieving a single item.
public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
{
IQueryable<T> query = _context.Set<T>();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query.Where(predicate).FirstOrDefault();
}
I am using it on a client table that has many jobs attached to it.
This is how I structured my code.
public ClientDetailsViewModel GetClientDetails(int id)
{
Client _client = _clientRepository
.GetSingle(c => c.Id == id, c => c.Creator, c => c.Jobs, c => c.State);
if(_client != null)
{
ClientDetailsViewModel _clientDetailsVM = mapClientDetailsToVM(_client);
return _clientDetailsVM;
}
else
{
return null;
}
}
The line:
.GetSingle(c => c.Id == id, c => c.Creator, c => c.Jobs, c => c.State);
successfully retrieves values for creator state and job.
However, nothing is retrieved for those related entities associated with the "jobs".
In particuar, JobVisits is a collection of visits to jobs.
For completeness I am adding the "job" and "jobvisit" entities below
public class Job : IEntityBase
{
public int Id { get; set; }
public int? ClientId { get; set; }
public Client Client { get; set; }
public int? JobVisitId { get; set; }
public ICollection<JobVisit> JobVisits { get; set; }
public int? JobTypeId { get; set; }
public JobType JobType { get; set; }
public int? WarrantyStatusId { get; set; }
public WarrantyStatus WarrantyStatus { get; set; }
public int? StatusId { get; set; }
public Status Status { get; set; }
public int? BrandId { get; set; }
public Brand Brand { get; set; }
public int CreatorId { get; set; }
public User Creator { get; set; }
....
}
public class JobVisit : IEntityBase
{
...
public int? JobId { get; set; }
public Job Job { get; set; }
public int? JobVisitTypeId { get; set; }
public JobVisitType VisitType { get; set; }
}
My question is, how do I modify the repository code above and my GetSingle use so that I can also load the related enitities JobVisit collection and the other related single entities Brand and JobType?
It is intended that navigation properties are not necessary retrieved for associated with the "jobs". That is why some properties are null. By default the .Include(property); goes only 1-level deep and that is a good thing. It prevents your query from fetching all the data of your database.
If you want to include multiple levels, you should use .ThenInclude(property) after .Include(property). From the documentation:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ToList();
}
My advice is that your method public T GetSingle(...) is nice and I would not change it in order to include deeper levels. Instead of that, you can simply use explicit loading. From the documentation:
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
context.Entry(blog)
.Collection(b => b.Posts)
.Load();
context.Entry(blog)
.Reference(b => b.Owner)
.Load();
}