I'm building an API in C# (.NET Core 6) and Entify Framework.
My models are like this:
public class Category
{
[Key]
public int Id { get; set; }
public string Name { get; set; } = null!;
}
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; } = null!;
[Column("cat_id")]
[ForeignKey("CategoryRef")]
public int CategoryId { get; set; };
public virtual Category CategoryRef { get; set; } = null!;
}
However, when I try to post a new Product like this:
{
"Name": "My product",
"CategoryId": 1
}
I get the following error:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-d1c8543428cbcf765f7f5610cfbe0fdf-e2d9938fe33e3aab-00",
"errors": {
"CategoryRef": [
"The CategoryRef field is required."
]
}
}
If I comment out the CategoryRef line (and the foreign key that points to it), it works fine (I have an existing Category in the database with Id = 1).
What am I doing wrong?
I solved it by adding:
builder.Services.AddControllers(options =>
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);
Related
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.
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()?
Hi there I am trying to learn WebAPI's with asp net core and ef core.I have simple 2 entities named Post and Category. I created relation as I added.
Category:
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public string CategoryTitle { get; set; }
public ICollection<Post> Posts { get; set; }
}
Post:
public class Post : BaseEntity
{
public int Id { get; set; }
public string AuthorName { get; set; }
public string PostTitle { get; set; }
public string PostContent { get; set; }
public int? CategoryId { get; set; }
public virtual Category Category { get; set; }
public EntryType Type { get; set; }
public ICollection<Comment> Comments { get; set; }
}
I thought a Category can have many posts about its field.
I used Fluent API in my Context class to create relation.
builder.Entity<Post>()
.HasOne<Category>(s => s.Category)
.WithMany(g => g.Posts)
.HasForeignKey(s => s.CategoryId);
In my controllers, I am adding new post with categoryId When I request categories, I cannot see posts that attached current category. I attached response:
{
"categoryId": 3,
"name": "Computer",
"title": "Technology",
"posts": []
}
Should I see something on posts key? When I check categories with id, posts property is null.But I added new post with categoryId = 3.
Here is my request body:
{
"author": "John Doe",
"title": "AspNetCore",
"content": "Backend",
"categoryId": 3,
"type": 2
}
I guess you do not include Posts while retrieving Categories. Try to include posts like this:
_context.Categories.Include(i=>i.Posts)
The Include method specifies the related objects to include in the query results. It can be used to retrieve some information from the database and also want to include related entities. Follow this link to learn about Lazy Loading and Eager Loading.
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'm trying to get properties of an object through my odata WCF service, but when i use $expand on a particular property, it appears to be "null". However, in another property I have the Id of that object (using Entityframework) and there you can see the Id is NOT empty.
The Model:
[Table("Approval"), DataContract, IgnoreProperties("Status", "Id")]
public class Approval : BaseEntity
{
...
[DataMember]
public Guid? ApproverId { get; set; }
[DataMember, ForeignKey("ApproverId")]
public virtual User Approver { get; set; }
[DataMember]
public virtual Request Request { get; set; }
}
[Table("Request"), DataContract, DataServiceKey("Id")]
public class Request : BaseEntity
{
...
[DataMember]
public Guid? LastModifiedById { get; set; }
[DataMember, ForeignKey("LastModifiedById")]
public virtual User LastModifiedBy { get; set; }
[DataMember]
public virtual Approval Approval { get; set; }
[DataMember, Required]
public Guid AbsenteeId { get; set; }
[DataMember, ForeignKey("AbsenteeId")]
public virtual User Absentee { get; set; }
[DataMember, Required]
public Guid AbsenceTypeId { get; set; }
[DataMember, ForeignKey("AbsenceTypeId")]
public virtual AbsenceType AbsenceType { get; set; }
}
The odata service call:
http://<servername>/DataService.svc/Approvals?&$format=json&$expand=Request,Request/Absentee,Request/LastModifiedBy,Request/AbsenceType,Approver&$skip=0&$top=1
The Returned Result:
{
"odata.metadata": "http://<servername>/DataService.svc/$metadata#Approvals",
"value": [
{
"Approver": null,
"Request": {
"LastModifiedBy": null,
"Absentee": null,
"AbsenceType": null,
...
"LastModifiedById": "6a1f8e79-99f5-e511-9444-0050568627d2",
"AbsenteeId": "6a1f8e79-99f5-e511-9444-0050568627d2",
"AbsenceTypeId": "fc7e8a5d-99f5-e511-9444-0050568627d2"
},
...
"ApproverId": null
}
]
}
In this case, value -> Request -> LastModifiedBy/Absentee/AbsenceType should NOT be empty!!!!
Anyone who knows what I missed/did wrong?