ASP.NET Core Web API working with Foreign Keys - api

I'm developing Web Api operations for User Controller and I don't understand why I'm getting the error in the POST method :
The "PK_Section" of the PRIMARY KEY constraint was violated. Unable to insert duplicate key into dbo.Section object. Duplicate key value: (3fa85f64-5717-4562-b3fc-2c963f66afa4).
My IdentityUser model:
public string MiddleName { get; set; }
[Required]
public string Surname { get; set; }
public DateTime BirthDate { get; set; }
public string Address { get; set; }
[Required]
public string UserType { get; set; }
public bool ParentsAgreement { get; set; }
public Section BelongSection { get; set; }
Section Model:
[Required]
public Guid Id { get; set; }
public string Name { get; set; }
public string CoachName { get; set; }
public string SportComplexTitle { get; set; }
[IgnoreDataMember]
public ICollection<User> UsersList { get; set; }
DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//section
modelBuilder.Entity<User>()
.HasOne(s => s.BelongSection)
.WithMany(a => a.UsersList);
modelBuilder.Entity<Section>()
.HasMany(s => s.UsersList)
.WithOne(a => a.BelongSection);
}
And my POST Method:
[HttpPost]
public async Task<ActionResult<User>> Add(User user)
{
try
{
if (user == null)
{
return BadRequest();
}
myDbContext.Users.Add(user);
await myDbContext.SaveChangesAsync();
var result = await myDbContext.Users.Include(o => o.BelongSection).FirstOrDefaultAsync(o
=> o.Id == user.Id);
return result;
}
catch (Exception)
{
throw;
}
}
So, I'm getting the error, that I can't use the Id of the existing already Section in creating User.
My Post method body is:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"userName": "string",
"normalizedUserName": "string",
"email": "string",
"normalizedEmail": "string",
"emailConfirmed": true,
"passwordHash": "string",
"securityStamp": "string",
"concurrencyStamp": "string",
"phoneNumber": "string",
"phoneNumberConfirmed": true,
"twoFactorEnabled": true,
"lockoutEnd": "2021-04-15T08:31:12.271Z",
"lockoutEnabled": true,
"accessFailedCount": 0,
"name": "string",
"middleName": "string",
"surname": "string",
"birthDate": "2021-04-15T08:31:12.271Z",
"address": "string",
"userType": "string",
"parentsAgreement": true,
"belongSection": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa4"
}
}

You could use two ways to add existing child to parent like below:
_context.Section.Attach(user.BelongSection);
Or:
_context.Entry(user.BelongSection).State = EntityState.Unchanged;
Be sure add AsNoTracking() to display the result.
Here is the whole working demo:
[HttpPost]
public async Task<ActionResult<User>> Add(User user)
{
try
{
if (user == null)
{
return BadRequest();
}
//_context.Section.Attach(user.BelongSection);
_context.Entry(user.BelongSection).State = EntityState.Unchanged;
_context.Users.Add(user);
await _context.SaveChangesAsync();
var result = await _context.Users.Include(o => o.BelongSection)
.AsNoTracking()
.FirstOrDefaultAsync(o=> o.Id == user.Id);
return result;
}
catch (Exception)
{
throw;
}
}
Note:
Not sure what is your asp.net core version, remember to add ReferenceLoopHandling.Ignore.
In asp.net core 3.x, System.Text.Json only supports serialization by value and throws an exception for circular references. So you need add Newtonsoft support like below:
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
In .Net 5.0,you could also add Newtonsoft support. Besides,System.Text.Json allows seting ReferenceHandler to Preserve:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-preserve-references?pivots=dotnet-5-0

Related

System.ArgumentException: 'Id field is not correctly populated'

I am trying to implement Redis, I am sending a put request from Postman and getting error.
Controller:
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.Created, Type = typeof(Brand))]
public async Task<IActionResult> Update([FromBody] Brand brand)
{
try
{
await _brandCollection.UpdateAsync(brand);
return Ok();
}
catch (Exception ex)
{
throw ex;
}
}
Error:
System.ArgumentException: 'Id field is not correctly populated'
From redis desktop manager I have confirmed that Id exists in record
Postman:
{
"Id": 1,
"Name": "Xiaomi",
"IsRegistered": "Yes",
"About": "Mobile Brand",
"Countries": [
"Pakistan",
"India",
"China"
]
}
Brand model class:
[Document(StorageType = StorageType.Json, Prefixes = new[] { "Brand" })]
public class Brand
{
[Indexed] public int Id { get; set; }
[Indexed] public string? Name { get; set; }
[Indexed] public string? IsRegistered { get; set; }
[Indexed] public string? About { get; set; }
[Indexed] public List<string>? Countries { get; set; }
}
I have injected HostedService as well in Program.cs:
builder.Services.AddHostedService<IndexCreationService>();
IndexCreationService:
public class IndexCreationService : IHostedService
{
private readonly RedisConnectionProvider _provider;
public IndexCreationService(RedisConnectionProvider provider)
{
_provider = provider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await _provider.Connection.CreateIndexAsync(typeof(Person));
await _provider.Connection.CreateIndexAsync(typeof(Category));
await _provider.Connection.CreateIndexAsync(typeof(Brand));
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

Better Way of Dealing With Circular References? - EF Core 3.1 and Web API

I am currently trying to progress with EF Core with a one-to-many (a user has many items). A tutorial or three later I managed to get things working with two very small and simple tables; however, I got a json exception: A possible object cycle was detected which is not supported which indicated that I had circular references.
Here is my code that gets around the issue using DTO objects, but is there a more cleaner way I can get around this issue as typing the, though it works, felt a bit wrong.
User:
namespace TestWebApplication.Database
{
public class User
{
[Key]
public int UserId { get; set; }
public string UserName { get; set; }
public string Dob { get; set; }
public string Location { get; set; }
public ICollection<Items> Items { get; set; }
}
}
Items:
namespace TestWebApplication.Database
{
public class Items
{
[Key]
public int ItemId { get; set; }
public string Item { get; set; }
public string Category { get; set; }
public string Type { get; set; }
public virtual User User { get; set; }
}
}
DtoItems:
namespace TestWebApplication.Database.DTOs
{
public class DtoItems
{
public string Item { get; set; }
public string Category { get; set; }
public string Type { get; set; }
public DtoUser User { get; set; }
}
}
DtoUser:
namespace TestWebApplication.Database.DTOs
{
public class DtoUser
{
public string UserName { get; set; }
public string Dob { get; set; }
public string Location { get; set; }
}
}
TestController:
[HttpGet]
[Route("getitems")]
public ActionResult<List<Items>> GetItems()
{
List<Items> items = _myContext.Items.Include(i => i.User).ToList();
// DTOs
List<DtoItems> dtoItems = new List<DtoItems>();
foreach (var i in items)
{
var dtoItem = new DtoItems
{
Item = i.Item,
Category = i.Category,
Type = i.Type,
User = new DtoUser
{
UserName = i.User.UserName,
Dob = i.User.Dob,
Location = i.User.Location
}
};
dtoItems.Add(dtoItem);
}
return Ok(dtoItems);
}
The output from endpoint:
[
{
"item": "xxx",
"category": "xxx",
"type": "xxx",
"user": {
"userName": "xxx",
"dob": "xxx",
"location": "xx"
}
},
{
"item": "xxx",
"category": "xxx",
"type": "xxx",
"user": {
"userName": "xxx",
"dob": "xxx",
"location": "xxx"
}
}
]
In my opinion, the use of a DTO is the correct way of dealing with this issue. The fact that your datamodel does not serialize well is trying to hint to you that you should not be serializing your datamodel from the API at all.
I think returning a DTO also solves further issues down the road (What if you want to return all properties of the UserModel except one, maybe it's a sensitive property you don't want to just return from your API, what if your UserModel in the DB gets more navigation properties that you don't want to return?).
There is really only two other ways of handling this.
You can switch to Newtonsoft.Json which has support for handling reference loops and you can configure it one single line
Like so :
services.AddControllers().AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
System.Text.Json does not have support for doing this (yet). Follow this Github Issue for more info : https://github.com/dotnet/runtime/issues/30820
You use the JsonIgnore attribute to force non serialization of properties which will work but... It looks weird to have an EntityFramework model have JSON Serialization options on it...
So your best bet, stick with the DTO.
More info :
https://dotnetcoretutorials.com/2020/03/15/fixing-json-self-referencing-loop-exceptions/
https://github.com/dotnet/runtime/issues/30820
https://www.newtonsoft.com/json/help/html/ReferenceLoopHandlingIgnore.htm

Duplicate Foreign Key entries being created on insert - EF Core and Web Api 3.1

I am currently learning how to work with EF Core with a simple one to many setup, where a user can have many items. In terms of retrieving the data from the tables, this is fine with some DTO models; however, when I try and add a user with multiple items via Postman, I noticed that for each item it had duplicated the user that many times (i.e. a user with 3 items will create 3 items and 3 users):
Postman (POST)
{
"username": "xxx",
"dob": "xxx",
"location": "xxx",
"items":[{
"item": "xxx",
"category": "xxx",
"type": "xxx"
},
{
"item": "xxx",
"category": "xxx",
"type": "xxx"
},
{
"item": "xxx",
"category": "xxx",
"type": "xxx"
}]
}
Context:
namespace TestWebApplication.Database
{
public class MyContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Items> Items { get; set; }
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
// erm, nothing here
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasMany(i => i.Items)
.WithOne(u => u.User);
}
public override int SaveChanges()
{
var entities = from e in ChangeTracker.Entries()
where e.State == EntityState.Added
|| e.State == EntityState.Modified
select e.Entity;
foreach (var entity in entities)
{
var validationContext = new ValidationContext(entity);
Validator.ValidateObject(entity, validationContext);
}
return base.SaveChanges();
}
}
}
Controller:
[HttpPost]
[Route("insertuseranditems")]
public ActionResult InsertUserAndItems(User user)
{
if (ModelState.IsValid)
{
using (MyContext myContext = _myContext as MyContext)
{
myContext?.Users?.Add(user);
int changes = myContext.SaveChanges();
if (changes > 0)
{
return Created("User saved", user);
}
}
}
return Accepted();
}
Items:
namespace TestWebApplication.Database
{
public class Items
{
[Key]
public int ItemId { get; set; }
public string Item { get; set; }
public string Category { get; set; }
public string Type { get; set; }
public virtual User User { get; set; }
}
}
Users:
namespace TestWebApplication.Database
{
public class User
{
[Key]
public int UserId { get; set; }
public string UserName { get; set; }
public string Dob { get; set; }
public string Location { get; set; }
public ICollection<Items> Items { get; set; }
}
}
I revisited my code and changed my Items.cs model to:
namespace TestWebApplication.Database
{
public class Items
{
[Key]
public int ItemId { get; set; }
public string Item { get; set; }
public string Category { get; set; }
public string Type { get; set; }
[ForeignKey("User")] // added this
public int? UserId { get; set; } // added this
public virtual User User { get; set; }
}
}
Now, when I send the above json via Postman, I get multiple items with the same user as a Foreign Key.
The site below seemed to help:
The ForeignKey Attribute

How to display Swashbuckle parameter object only with fields that should actually be sent?

I'm starting to work with Swagger using the Swashbuckle library for AspNetCore.
And when putting in my API an object with references it presents as if it were necessary to send all the fields of the references, when only the identifier (Id)
Here's an example:
Model Structure:
public class Cidade
{
public long Id { get; set; }
public string Nome { get; set; }
public Uf Uf { get; set; }
}
public class Uf
{
public long Id { get; set; }
public string Nome { get; set; }
public Pais Pais { get; set; }
}
public class Pais
{
public long Id { get; set; }
public string Nome { get; set; }
}
And the following API:
[Produces("application/json")]
[Route("api/Cidade")]
public class CidadeController : Controller
{
// POST: api/Cidade
[HttpPost]
public void Post([FromBody]Cidade value)
{
}
}
The result in Swagger is as follows:
And what I would like is the following (only up to uf.id):
How can I get this result?
I followed the logic of #HelderSepu answer, to get my solution, which would be as follows:
I built a Schema filter to add an example to the reference properties (Ref), which has a property called "Id":
public class ApplySchemaRefIdExtensions : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema.Properties != null)
{
foreach (var p in schema.Properties)
{
if (p.Value.Example == null && p.Value.Ref != null)
{
var reference = context.SystemType.GetProperty(p.Value.Ref.Split("/").LastOrDefault());
if (reference != null)
{
var id = reference.PropertyType.GetProperty("Id");
if (id != null)
{
p.Value.Example = new
{
Id = 123
};
p.Value.Ref = null;
}
}
}
}
}
}
}
On Startup.cs:
services.AddSwaggerGen(c =>
{
// ...
c.SchemaFilter<ApplySchemaRefIdExtensions>();
});
Result for the same example of the question:
I was looking on my samples and I think I found something you can use:
http://swagger-net-test.azurewebsites.net/swagger/ui/index?filter=P#/PolygonVolume/PolygonVolume_Post
On my case I'm adding more, you need less, but still what you need is just a custom example...
the JSON looks like this:
"PolygonVolumeInsideParameter": {
"properties": {
"Points": {
"items": {
"$ref": "#/definitions/Location"
},
"xml": {
"name": "Location",
"wrapped": true
},
"example": [
{
"Lat": 1.0,
"Lon": 2.0
},
{
"Lat": 5.0,
"Lon": 6.0
}
],
"type": "array"
},
"PlanId": {
"type": "string"
}
},
"xml": {
"name": "PolygonVolumeInsideParameter"
},
"type": "object"
},
And on swashbuckle I added the example it with an ISchemaFilter my code is here:
https://github.com/heldersepu/Swagger-Net-Test/blob/master/Swagger_Test/App_Start/SwaggerConfig.cs#L891

ASP.NET core POST request fail

I have a model:
public class CoreGoal
{
[Key]
public long CoreGoalId { get; set; }
public string Title { get; set; }
public string Effect { get; set; }
public string Target_Audience { get; set; }
public string Infrastructure { get; set; }
public virtual ICollection<Benefit> Benefits { get; set; }
public virtual ICollection<Step> Steps { get; set; }
public virtual ICollection<Image> Images { get; set; }
public virtual ICollection<SubGoal> SubGoals { get; set; }
public CoreGoal()
{
}
}
And Image model is as following:
public class Image
{
[Key]
public long ImagelId { get; set; }
public byte[] Base64 { get; set; }
[ForeignKey("CoreGoalId")]
public long CoreGoalId { get; set; }
public Image()
{
}
}
My controller class:
[Route("api/[controller]")]
public class CoreGoalController : Controller
{
private readonly ICoreGoalRepository _coreGoalRepository;
//Controller
public CoreGoalController(ICoreGoalRepository coreGoalRepository) {
_coreGoalRepository = coreGoalRepository;
}
//Get methods
[HttpGet]
public IEnumerable<CoreGoal> GetAll()
{
return _coreGoalRepository.GetAllCoreGoals();
}
[HttpGet("{id}", Name = "GetCoreGoal")]
public IActionResult GetById(long id)
{
var item = _coreGoalRepository.Find(id);
if (item == null)
{
return NotFound();
}
return new ObjectResult(item);
}
//Create
[HttpPost]
public IActionResult Create([FromBody] CoreGoal item)
{
if (item == null)
{
return BadRequest();
}
_coreGoalRepository.CreateCoreGoal(item);
return CreatedAtRoute("GetCoreGoal", new { id = item.CoreGoalId }, item);
}
}
Repository:
public class CoreGoalRepository : ICoreGoalRepository
{
private readonly WebAPIDataContext _db;
public CoreGoalRepository(WebAPIDataContext db)
{
_db = db;
}
//Add new
public void CreateCoreGoal(CoreGoal coreGoal)
{
_db.CoreGoals.Add(coreGoal);
_db.SaveChanges();
}
//Get all
public IEnumerable<CoreGoal> GetAllCoreGoals()
{
return _db.CoreGoals
.Include(coreGoal => coreGoal.Benefits)
.Include(coreGoal => coreGoal.Steps)
.Include(coreGoal => coreGoal.Images)
.Include(coreGoal => coreGoal.SubGoals)
.ToList();
}
//Find specific
public CoreGoal Find(long key)
{
return _db.CoreGoals.FirstOrDefault(t => t.CoreGoalId == key);
}
}
public interface ICoreGoalRepository
{
void CreateCoreGoal(CoreGoal coreGoal);
IEnumerable<CoreGoal> GetAllCoreGoals();
CoreGoal Find(long key);
void DeleteCoreGoal(long id);
void UpdateCoreGoal(CoreGoal coreGoal);
}
When I do a POST request from swagger I get a template like:
{
"coreGoalId": 0,
"title": "string",
"effect": "string",
"target_Audience": "string",
"infrastructure": "string",
"benefits": [
{
"benefitId": 0,
"what": "string",
"coreGoalId": 0
}
],
"steps": [
{
"stepId": 0,
"what": "string",
"coreGoalId": 0
}
],
"images": [
{
"imagelId": 0,
"base64": "string",
"coreGoalId": 0
}
],
"subGoals": [
{
"subGoalId": 0,
"title": "string",
"priority": "string",
"audience": "string",
"social_aspects": "string",
"coreGoalId": 0,
"issues": [
{
"issueId": 0,
"title": "string",
"subGoalID": 0
}
]
}
]
}
If I POST like like this, my request fails with status 400, however if I remove
"images": [
{
"imagelId": 0,
"base64": "string",
"coreGoalId": 0
}
],
from this request, then it is successful. Why is it happening? All other models i.e. Benefit, Step are exactly identical to Image in structure.
UPDATE:
Changing base64 type from byte[] to string eliminates this problem but in that case while saving to my MySql database the big base64 string is chopped and kind of becomes useless to again form the image.