I'm strugglish with adding feature for my controller. While adding new item, receving the error like: "An error occurred while updating the entries. See the inner exception for details."
I debugged it, and understood ProductDetailIs is null and here is the issue. But, can not figure out how to mend the problem.
Here is the DTO models:
public class WishlistItemDto
{
public int Id { get; set; }
public string CustomerId { get; set; }
public ProductDetailsDtoWithPrimaryImage ProductDetails { get; set; }
public int Quantity { get; set; }
}
public class WishListItemCreationDto
{
public string CustomerId { get; set; }
public int ProductDetailId { get; set; }
public int Quantity { get; set; }
}
Controller:
[HttpPost]
public async Task<IActionResult> Add(WishListItemCreationDto wishListItemDto)
{
var itemAdd = _mapper.Map<WishlistItemDto>(wishListItemDto);
var itemCreated = await _wishListItemService.AddAsync(itemAdd);
return CreatedAtAction(nameof(GetId), new { id = itemCreated.Id }, wishListItemDto);
}
Service:
public async Task<WishlistItemDto> AddAsync(WishlistItemDto item)
{
var entity = _mapper.Map<WishlistItem>(item);
await _wishListItemRepository.AddAsync(entity);
return _mapper.Map<WishlistItemDto>(entity);
}
Repository:
public async Task<WishlistItem> AddAsync(WishlistItem item)
{
await _context.Set<WishlistItem>().AddAsync(item);
await _context.SaveChangesAsync();
return item;
}
This line here:
var itemAdd = _mapper.Map<WishlistItemDto>(wishListItemDto);
your "wishListItemDto" is passed in as a 'WishListItemCreationDto' which contains only a ProductDetailsId. Automapper will have no way of knowing how to convert that into a ProductDetailsDtoWithPrimaryImage.
Typically for something like this where you pass an reference ID you would compose your entity by either populating a FK or loading the referenced entity. Your existing service and repository patterns will complicate your final solution. From what I can see from your example I'd look at creating an AddAsync method that accepts the WishListItemCreationDto:
public async Task<WishlistItemCreationDto> AddAsync(WishlistItemCreationDto item)
{
var entity = _mapper.Map<WishlistItem>(item);
var productDetails = _productDetailsRepository.GetById(item.ProductDetailsId);
entity.ProductDetails = productDetails;
await _wishListItemRepository.AddAsync(entity);
return _mapper.Map<WishlistItemDto>(entity);
}
Without the added abstraction complexity of the Service and Repository the add operation can be a whole lot simpler:
[HttpPost]
public async Task<IActionResult> Add(WishListItemCreationDto wishListItemDto)
{
// or better, use an injected dependency to the Context...
// TODO: add applicable exception handling.
using(var context = new AppDbContext())
{
var item = _mapper.Map<WishlistItem>(wishListItemDto);
var productDetails = context.ProductDetails.Single(x => x.ProductDetailsId == wishListItemDto.ProductDetailsId);
item.ProductDetails = productDetails;
context.SaveChanges();
return CreatedAtAction(nameof(GetId), new { id = itemCreated.Id }, wishListItemDto);
}
}
Related
I am a bit new to REST API. I have the below controller created to mock an API Service called by a client code under Test. I need to return the response in JSON format as mentioned in the query and need some help to fix it.
Controller:
[HttpPost]
[ApiKeyAuth]
[ValidateModel]
[ProducesResponseType(typeof(Item), StatusCodes.Status201Created)]
public async Task<IActionResult> AddNewItem([FromBody] Item item)
{
var itemId = await _repo.AddItemAsync(item);
return CreatedAtAction(nameof(GetItemById), new { itemId, controller = "Example" },
itemId);
}
Contract:
public interface IExampleControllerRepository
{
Task<int> AddItemAsync(Item item);
}
Repo:
public class ExampleRepository : IExampleControllerRepository
{
private readonly ExampleDbContext _context;
public ExampleRepository(ExampleDbContext context) => _context = context;
public async Task<int> AddItemAsync(Item item)
{
_context.Add(item);
await _context.SaveChangesAsync();
return item.ItemId;
}
}
Expected positive response template:
{"response":{"status":0,"data":[{"id":"1234"}]}}
// Summary:
// Creates a Microsoft.AspNetCore.Mvc.CreatedAtActionResult object that produces
// a Microsoft.AspNetCore.Http.StatusCodes.Status201Created response.
//
// Parameters:
// actionName:
// The name of the action to use for generating the URL.
//
// routeValues:
// The route data to use for generating the URL.
//
// value:
// The content value to format in the entity body.
//
// Returns:
// The created Microsoft.AspNetCore.Mvc.CreatedAtActionResult for the response.
[NonAction]
public virtual CreatedAtActionResult CreatedAtAction(string? actionName, object? routeValues, [ActionResultObjectValue] object? value)
{
throw null;
}
The response body's template is related to the type of value, So if you want to get the response template like:
{"response":{"status":0,"data":[{"id":"1234"}]}}
You need to pass a value of a specific type instead of itemId.
Here is a simple demo.
public class Test
{
public List<response> responses { get; set; }
}
public class response
{
public int status { get; set; }
public List<test1> data { get; set; }
}
public class test1
{
public string Id { get; set; }
}
For testing convenience, I just hard code here.
[HttpPost]
[ApiKeyAuth]
[ValidateModel]
[ProducesResponseType(typeof(Item), StatusCodes.Status201Created)]
public async Task<IActionResult> AddNewItem([FromBody] Item item)
{
Test test = new Test()
{
responses = new List<response>()
{
new response()
{
status = 0,
data = new List<test1> {
new test1()
{
Id = "1234",
}
}
}
}
};
var itemId = await _repo.AddItemAsync(item);
return CreatedAtAction(nameof(GetItemById), new { Id = item.ItemId }, test);
}
Result
I have the following setup:
The document:
[BsonCollection("Users")] // I get the collection name with a custom extension
[BsonIgnoreExtraElements]
public class UserDocument
{
[BsonId]
public Guid Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public UserSettingsModel UserSettings { get; set; }
}
public class UserSettingsModel
{
// ...
}
The repository:
public class UserRepository
{
private readonly IMongoCollection<UserDocument> _collection;
private readonly ILogger<UserRepository> _logger;
public UserRepository(IMongoDatabase database, ILogger<UserRepository> logger)
{
// returns "Users"
var collectionName = typeof(UserDocument).GetCollectionName();
_collection = database.GetCollection<UserDocument>(collectionName);
}
// ...
public async Task<UserDocument> GetById(Guid id)
{
var filter = Builders<UserDocument>.Filter.Eq(x => x.Id, id);
var user = await _collection.FindAsync(filter);
// var user = await _collection.FindAsync(x => x.Id == id); - doesn't work either
var request = filter.Render(
_collection.DocumentSerializer,
_collection.Settings.SerializerRegistry).ToString();
_logger.LogDebug(request);
return user.FirstOrDefault();
}
}
And I initialize the client this way:
// ...
BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));
var client = new MongoClient(connectionString);
var database = client.GetDatabase(dbName);
services.AddSingleton(c => database);
// convention pack and registries
// ...
// if moved here doesn't work either
// BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));
The filter generated in GetById is still the following: { "_id" : CSUUID("459f165a-4a91-4f39-906c-dc7401ee2468") } when I expect it to be UUID instead of CSUUID.
So, the query doesn't find anything and returns null. In the database the document I'm searching for has _id: UUID('459f165a-4a91-4f39-906c-dc7401ee2468')
What am I doing wrong?
I was able to fixing by this trick:
var mongoConnectionUrl = new MongoUrl(connectionString);
var mongoClientSettings = MongoClientSettings.FromUrl(mongoConnectionUrl);
// before initializing client
mongoClientSettings.GuidRepresentation = GuidRepresentation.Standard;
However setting the GuidRepresentation in client settings is obsolete, which is quite confusing. Also, the query generated still has CSUUID instead of UUID. I was able to log the query the following way:
mongoClientSettings.ClusterConfigurator = cb =>
{
cb.Subscribe<CommandStartedEvent>(e => logger.LogDebug($"{e.CommandName} - {e.Command.ToJson()}"));
};
If anyone finds a better way and post it here it would be appreciated.
I have a Kalem Entity with a collection of DigerKalemMaliyetleri property, which is a collection of MaliyetBirimi objects. DigerKalemMaliyetleri is of JSON type and stored at the same table as a JSON column.
public class Kalem
{
public int Id { get; set; }
[Column(TypeName = "json")]
public ICollection<MaliyetBirimi> DigerKalemMaliyetleri { get; set; }
}
public class MaliyetBirimi
{
public int? DovizCinsi { get; set; }
public decimal? Maliyet { get; set; }
}
When I try to update entity with only DigerKalemMaliyetleri property changed:
DataContext.Entry<Kalem>(first).CurrentValues.SetValues(second);
SQL Update command isn't executed and database record is not updated.
How could I update the entity without explicitly setting DigerKalemMaliyetleri property?
Regards
I had the same problem, you cann't actually use SetValues to update navigation property, you nead instead use DataContext.Update(YourNewObj) and then DataContext.SaveChanges();, or if you want to use SetValues approach, you need:
-Get the exist entry
Kalem existObj = DataContext.Kalems.Find(YourNewObj.Id);
-Loop in navigations of updating entry and the existing one to set the values of updating entry:
foreach(var navObj in DataContext.Entry(YourNewObj).Navigations)
{
foreach(var navExist in DatatContext.Entry(existObj).Navigations)
{
if(navObj.Metadata.Name == navExist.MetaData.Name)
navExist.CurrentValue = navObj.CurrentValue;
}
}
-Update also changes of direct properties:
DataContext.Entry(existObj).CurrentValues.SetValues(YourNewObj);
-Save your Updating:
DataContext.SaveChanges();
You can also check if you need to load your Navigations before going in foreach loop, otherwise you will get an error.
Please if you see beter scenario, correct me.
It's hard to know exactly what you're doing without a complete code sample. Note also that you're trying to set all properties of first from second, including e.g. the Id, which is probably not what you want.
Here's a complete code sample which works for me:
await using (var ctx = new BlogContext())
{
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();
ctx.Kalem.Add(new()
{
DigerKalemMaliyetleri = new List<MaliyetBirimi>()
{
new() { DovizCinsi = 1, Maliyet = 2 }
}
});
await ctx.SaveChangesAsync();
}
await using (var ctx = new BlogContext())
{
var first = ctx.Kalem.Find(1);
var second = new Kalem
{
DigerKalemMaliyetleri = new List<MaliyetBirimi>()
{
new() { DovizCinsi = 3, Maliyet = 4 }
}
};
ctx.Entry(first).Property(k => k.DigerKalemMaliyetleri).CurrentValue = second.DigerKalemMaliyetleri;
await ctx.SaveChangesAsync();
}
public class BlogContext : DbContext
{
public DbSet<Kalem> Kalem { get; set; }
static ILoggerFactory ContextLoggerFactory
=> LoggerFactory.Create(b => b.AddConsole().AddFilter("", LogLevel.Information));
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseNpgsql(#"Host=localhost;Username=test;Password=test")
.EnableSensitiveDataLogging()
.UseLoggerFactory(ContextLoggerFactory);
}
public class Kalem
{
public int Id { get; set; }
[Column(TypeName = "json")]
public ICollection<MaliyetBirimi> DigerKalemMaliyetleri { get; set; }
}
public class MaliyetBirimi
{
public int? DovizCinsi { get; set; }
public decimal? Maliyet { get; set; }
}
We have an API with about a dozen integration tests. All the tests passed until I added some DTOs and used AutoMapper. Now, all the tests that test methods that use AutoMapper and the DTOs are failing. I have provided all the code needed to understand one of the failing tests. Also, I read a lot about AutoMapper and the following StackOverflow posts:
Integration Testing with AutoMapper fails to initialise configuration
A kind of integration testing in ASP.NET Core, with EF and AutoMapper
Startup.cs
This is our Startup.ConfigureServices(). I have tried every code block commented out and/or marked "ATTEMPTED".
public void ConfigureServices(IServiceCollection services)
{
services
.AddDbContext<OurContext>(options =>
options.UseSqlServer(Configuration["ConnectionString"]))
.AddDbContext<OurContext>()
.AddRazorPages()
.AddMvcOptions(options => options.EnableEndpointRouting = false)
.AddNewtonsoftJson(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
services
.AddControllersWithViews();
//ATTEMPTED
//services
// .AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
//ATTEMPTED
//MapperConfiguration mapperConfiguration = new MapperConfiguration(mc =>
//{
// mc.AddProfile(new OurProfile());
//});
//IMapper mapper = mapperConfiguration.CreateMapper();
//services
// .AddSingleton(mapper);
//ATTEMPTED
//services
// .AddAutoMapper(typeof(Startup));
//ATTEMPTED
//var assembly = typeof(Program).GetTypeInfo().Assembly;
//services
// .AddAutoMapper(assembly);
//ATTEMPTED
var assembly = typeof(Program).GetTypeInfo().Assembly;
services.AddAutoMapper(cfg =>
{
cfg.AllowNullDestinationValues = true;
cfg.CreateMap<OurModel, OurDto>()
.IgnoreAllPropertiesWithAnInaccessibleSetter();
}, assembly);
}
Controller
This is our controller.
[Route("api/[controller]")]
[ApiController]
public class OurController : ControllerBase
{
private readonly OurContext _context;
protected readonly ILogger<OurController> Logger;
private readonly IMapper _mapper;
public OurController(OurContext context, ILogger<OurController> logger,
IMapper mapper)
{
_context = context ??
throw new ArgumentNullException(nameof(context));
Logger = logger ??
throw new ArgumentNullException(nameof(logger));
_mapper = mapper ??
throw new ArgumentNullException(nameof(mapper));
}
[HttpGet]
public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
{
IQueryable<OurModel> models = _context.OurModel;
IQueryable<OurDto> dtos =
_mapper.Map<IQueryable<OurDto>>(models);
return await dtos.ToListAsync();
}
}
Profile, Model, and DTO
Profile
public class OurProfile : Profile
{
public OurProfile()
{
CreateMap<OurModel, OurDto>();
}
}
Model
public partial class OurModel
{
public string Number { get; set; }
public string Name1 { get; set; }
public string Name2 { get; set; }
public string Status { get; set; }
public DateTime? Date { get; set; }
public string Description { get; set; }
public string Comment { get; set; }
public string District { get; set; }
}
DTO
public class OurDto
{
public string Number { get; set; }
public string Name1 { get; set; }
public string Name2 { get; set; }
public string Status { get; set; }
public DateTime? Date { get; set; }
public string Description { get; set; }
public string Comment { get; set; }
public string District { get; set; }
}
Test Fixture
This is our test fixture.
public abstract class ApiClientFixture : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
protected abstract string RelativeUrl { get; }
protected ApiClientFixture(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
protected HttpClient CreateClient()
{
HttpClient client;
var builder = new UriBuilder();
client = _factory.CreateClient();
builder.Host = client.BaseAddress.Host;
builder.Path = $"{RelativeUrl}";
client.BaseAddress = builder.Uri;
return client;
}
}
Test
This is our test class. The single test in this test class fails.
public class Tests : ApiClientFixture
{
public Tests(WebApplicationFactory<Startup> factory) : base(factory)
{
}
protected override string RelativeUrl => "api/OurController/";
[Fact]
public async void GetAllReturnsSomething()
{
var response = await CreateClient().GetAsync("");
Assert.True(response.IsSuccessStatusCode);
}
}
When I debug the test I see that a 500 status code is returned from the URL provided to the in-memory API.
Does anybody have some suggestions? More than half of our tests currently fail, and I suspect that AutoMapper is not configured properly for integration testing.
Creating a map for IQueryable<T> is not really a good solution. In your answer you are losing proper flow of asynchronous database querying. I wrote about IQueryable<T> in a comment because you were looking for a 500 error cause. Making it work it's a one thing, making it a good solution it's another thing, however.
I'd strongly suggest to use AutoMapper ProjectTo() extension which you can use directly on a IQueryable<T> sequence. It let's you combine mapping and querying in one go. More or less it does a Select() based on your mappings, so it not only gives you proper model right away with the query result, but it also reduces the amount of columns obtained from database, which can make the query run faster. But, there are of course limitations to it, e.g. you can't use custom type converters or conditional mapping. You can read more about Project() in the documentation.
Usage:
public async Task<ActionResult<List<OurDto>>> GetAll()
{
return await _context
.OurModel
.ProjectTo<OutDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
Thanks to #Prolog for his comment. I realized that I need to map each element of the IQueryable individually, so I rewrote my Controller method.
Also, side note: IList.AsQueryable().ToListAsync() does not work, so I wrote:
IQueryable<OurDto> dtosQueryable = dtos.AsQueryable();
return await Task.FromResult(dtosQueryable.ToList());
Old Controller Method
[HttpGet]
public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
{
IQueryable<OurModel> models = _context.OurModel;
IQueryable<OurDto> dtos =
_mapper.Map<IQueryable<OurDto>>(models);
return await dtos.ToListAsync();
}
New Controller Method
public async Task<ActionResult<IEnumerable<OurDto>>> GetAll()
{
IQueryable<OurModel> models = _context.OurModel;
IList<OurDto> dtos = new List<OurDto>();
foreach (OurModel model in models)
{
OurDto dto = _mapper.Map<OurDto>(model);
dtos.Add(dto);
}
IQueryable<OurDto> dtosQueryable = dtos.AsQueryable();
return await Task.FromResult(dtosQueryable.ToList());
}
How to you get a string representation of the ObjectId returned via ASP.NET Core.
I have the following result of an action in my controller:
return new ObjectResult(new { session, user });
One of the user properties is the UserId that is of the ObjectId type.
However, this gets returned in the response as
"id": {
"timestamp": 1482840000,
"machine": 6645569,
"pid": 19448,
"increment": 5052063,
"creationTime": "2016-12-27T12:00:00Z"
}
I would like the response to simply be 58625d5201c4f202609fc5f3 that is the string representation of the same structure.
Are there any easy way to do this for all returned ObjectIds?
EDIT
Adding some more data
Here are the user class. ObjectId is MongoDB.Bson.ObjectId
public class User
{
public ObjectId Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string PasswordSalt { get; set; }
}
The get method in my controller. Controller is Microsoft.AspNetCore.Mvc.Controller.
[HttpGet("{id}", Name = "GetUser")]
public async Task<IActionResult> Get(ObjectId id)
{
var user = await _repository.GetOne<User>(id);
if (user == null) return NotFound();
return new ObjectResult(user);
}
And this is the method from my repository:
public async Task<T> GetOne<T>(ObjectId id)
{
var collectionname = typeof(T).Name;
var collection = _database.GetCollection<T>(collectionname);
var filter = Builders<T>.Filter.Eq("_id", id);
var result = await collection.Find(filter).ToListAsync();
return result.FirstOrDefault();
}
You have to explicitly write a ObjectID to JSON convertor. Please check the link below:
https://stackoverflow.com/a/37966098/887976