I have a controller post action which has two parameters, and I want to use the first to update the database using TryUpdateModelAsync (the second is some rendering parameter).
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Amendment amendment, string format)
{
var amendmentToUpdate = await _context.Amendments
.SingleOrDefaultAsync(a => a.Id == amendment.Id);
if (ModelState.IsValid)
{
try
{
await TryUpdateModelAsync<Amendment>(
amendmentToUpdate,
"",
a => a.Title, a => a.Date);
await _context.SaveChangesAsync();
...
It seems that the TryUpdateModelAsync finds the controller context and automatically updates these fields, but since I'm passing in two parameters, it doesn't know that it should take these from amendment. Is there a way to pass in the "source object" to TryUpdateModelAsync?
Related
My main page has an ObservableCollection (defined in a ViewModel) holding objects (defined in a Model) and I have other pages, one that shows details and one to add a new object. I use the MVVM CommunityToolkit.
I can pass one object from the collection to the details page:
[RelayCommand]
async Task GoToDetails(DailyScrum scrum)
{
if (scrum is null)
return;
await Shell.Current.GoToAsync($"{nameof(View.ScrumDetailsPage)}", true,
new Dictionary<string, object>
{
{"Scrum", scrum }
});
}
However, I am not able to pass the whole collection to the "Add" page, add the new object and pass it back:
[RelayCommand]
async Task AddNewScrum(ObservableCollection<DailyScrum> scrums)
{
if (scrums is null)
return;
await Shell.Current.GoToAsync($"{nameof(View.AddScrumPage)}", true,
new Dictionary<string, object>
{
{"Scrums", scrums }
});
}
How can I do this? Or is it a wrong attempt to pass around the collection? Can I write access the collection from another viewmodel?
If the ObservableCollection's size is small, you can try to use the Newtonsoft.Json package to convert it to string and then pass it. Such as:
var jsonstring = JsonConvert.SerializeObject(scrums);
Shell.Current.GoToAsync($"{nameof(View.AddScrumPage)}?param={jsonstring}");
And convert the string to ObservableCollection with the following code:
ObservableCollection<DailyScrum> data = JsonConvert.DeserializeObject<ObservableCollection<DailyScrum>>(jsonstring);
If the collection's size is big, you can store the string in the Preferences. Such as:
var jsonstring = JsonConvert.SerializeObject(scrums);
Preferences.Set("Data", jsonstring);
And get the data in the AddScrumPage:
bool hasKey = Preferences.ContainsKey("Data");
var content = Preferences.Get("Data", string.Empty);
ObservableCollection<DailyScrum> data = hasKey ? JsonConvert.DeserializeObject<Model>(content) : null;
Update
You can try to use the Shell.Current.Navigation, it has the same effect as the Shell.Current.GoToAsync, such as:
[RelayCommand]
async Task GoToDetails(DailyScrum scrum)
{
if (scrum is null)
return;
await Shell.Current.Navigation.PushAsync(new View.ScrumDetailsPage(scrums))
}
I have a little problem. But I dont know why it doesnt work. And I dont know how to post all ids by postman.
I am using unit of work with generic repository. I want to send int[] ids to my controller. I dont want to send entity. I searched a lot it today. And I changed my code. But what is problem now?
This is my repostiroy:
public async Task DeleteRangeAsync(Expression<Func<T, bool>> predicate)
{
IQueryable<T> query = _dbSet.Where(predicate);
await Task.Run(() => { _dbSet.RemoveRange(query.AsNoTracking()); });
}
This is my KulturManager:
public async Task<IResult> HardDeleteRangeAsync(int[] ids)
{
await UnitOfWork.Kulturs.DeleteRangeAsync(c => ids.Contains(c.Id));
await UnitOfWork.SaveAsync();
return new Result(ResultStatus.Success, Messages.Info("Kultur", "HardDelete"));
}
And this is my KulturController:
[HttpDelete("{ids}")]
public async Task<IActionResult> HardDeleteRangeAsync(int[] ids)
{
var result = await _kulturManager.HardDeleteRangeAsync(ids);
return Ok(result.Message);
}
Thank you for help
You shouldn't fetch all the entities you want to delete. Instead create stub entities for RemoveRange. If you don't have a common base class, this requires reflection, but with a common entity base class you can do it like this:
public void DeleteRange<T>(int[] ids) where T: BaseEntity, new()
{
_dbSet.RemoveRange(ids.Select(i => new T() { Id = i }).ToList());
}
or if the method is in a generic class, the method would look like
public void DeleteRange(int[] ids)
{
_dbSet.RemoveRange(ids.Select(i => new T() { Id = i }).ToList());
}
And there's no reason to mark this as Async now since it doesn't do any database access.
I'm trying to run some unit tests with inmemory database. I'm creating a new entity and then I'm checking if it exists or not. If I query as an Task<IEnumerable<Entity>> or a Task<Entity> with FindAsync(id),I get the model, but if I check with IQueryable<Entity> I'm getting null. Is there a restriction with InMemory Db with IQueryables? Because I feel that it only fetches it because of FindAsync();
Below is the code:
var model = await _context.User.FindAsync(id); returns model
var model = await GetUserModel(id);
Task<User> GetUserModel(int id)=>
await _context.User.FindAsync(id);
returns model
IQueryable<User> GetUser() =>
_context.User
.Include(u => u.Role)
.AsQueryable();
var model = await GetUser().AsNoTracking().FirstOrDefaultAsync(u => u.Id == id);
returns null
UnitTestSetup
protected static UserDbContext GetContext()
{
var options = new DbContextOptionsBuilder<UserDbContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
return new UserDbContext(options);
}
UnitTest
[Fact]
public async Task GetUsers_ShouldReturnUsers()
{
//Arrange
var userService = new UserService(GetContext());
await userService.CreateUser(Helpers.CreateUser());
//Act
var viewModel = await userService.GetUsers();
//Assert
viewModel.Should().NotBeNullOrEmpty();
}
Edit:
After many tests it seems that my instinct is correct. If I use FirstOrDefaultAsync() it doesn't find the in memory created entity but if I use FindAsync() it does. If I compare a value of the created model with the model that FindAsync() fetches it returns true, so I know that my IQueryable method should be working.
In my repository I'm trying to use eager loading to load related entities. Not sure why but it seems like when I return all instances of a particular entity, related entities are returned, but when I limit the results returned the related entities are not included in the results.
This code in the service layer is returning all orders, including related Customer, OrderItem, and Product entities:
public async Task<IEnumerable<Order>> GetOrdersAsync()
{
return await _repository.GetAsync(null, q => q.OrderByDescending(p => p.CreatedDate), "Customer", "OrderItems", "OrderItems.Product");
}
In the repository:
public async Task<IEnumerable<Order>> GetAsync(Expression<Func<Order, bool>> where = null, Func<IQueryable<Order>, IOrderedQueryable<Order>> orderBy = null, params string[] navigationProperties)
{
IQueryable<Order> query = _context.Set<Order>();
if (where != null)
{
query = query.Where(where);
}
//Apply eager loading
foreach (string navigationProperty in navigationProperties)
query = query.Include(navigationProperty);
if (orderBy != null)
{
return await orderBy(query).ToListAsync();
}
else
{
return await query.ToListAsync();
}
}
This code in the service layer is getting an order by id, but for whatever reason is not returning related Customer, OrderItem, and Product entities:
public async Task<Order> GetOrderByIdAsync(long id)
{
return await _repository.GetByIdAsync(id, "Customer", "OrderItems", "OrderItems.Product");
}
In the repository:
public async Task<Order> GetByIdAsync(long id, params string[] navigationProperties)
{
DbSet<Order> dbSet = _context.Set<Order>();
foreach (string navigationProperty in navigationProperties)
dbSet.Include(navigationProperty);
return await dbSet.FindAsync(id);
}
The one difference I see between the two repository methods is one is casting the _context.Set() to IQueryable before including navigation properties, while the other is calling Include directly on the DbSet itself. Should this matter?
So the repository method that was not working was using DBSet.Include incorrectly. Include is actually a method on the DBQuery class, which DBSet inherits from. I needed to use DBQuery.Include to replace the query with a new instance of the query for each navigation property I included. So I changed the implementation to:
public async Task<Order> GetByIdAsync(long id, params string[] navigationProperties)
{
DbQuery<Order> dbQuery = _context.Set<Order>();
foreach (string navigationProperty in navigationProperties)
dbQuery = dbQuery.Include(navigationProperty);
return await dbQuery.Where(o => o.Id == id).FirstOrDefaultAsync();
}
While using MongoDB C# driver with WebApi I came to the following problem. When I want to read all documents (or even just one) from the database the repo's function will get the correct data but in WebApi the object returned from the repo causes a stack overflow. I suspect that I am doing something wrong with the way the objects are returned.
WebApi where the Repo's method is called:
// GET api/<controller>
public async Task<List<Event>> Get()
{
return await _repo.FindAll();
}
// GET api/<controller>/5
public async Task<Event> Get(string id)
{
Event e = await _repo.FindById(id);
return e;
}
And corresponding methods in the Repo:
public async Task<Event> FindById(string id)
{
Event e = await _collection.Find<Event>(x => x.ID == ObjectId.Parse(id)).FirstAsync();
return e;
}
public async Task<List<Event>> FindAll()
{
var filter = new BsonDocument();
List<Event> list = await _collection.Find(filter).ToListAsync();
return await Task<List<Event>>.FromResult(list);
}
Thanks for all the help in advance!
Edit: I found that when I return string from the function instead of Event the whole thing works.
What I think is making problems is the ID property in the Event.
The problem was that the Event had an ObjecId property. The JSON.Net doesn't know about that type. See the solution here: JSON.NET cast error when serializing Mongo ObjectId