0

The application was built on a bunch of asp .net core mvc and entity framework. I have a map with markers on it. I want to change the parameters of a certain object through textboxes. The request from the frontend is written in axios, and it works flawlessly. From the first time I get the changes in the database. (mysql, provider: pomelo.mysql). When I try to access the get request for the first time, I get the old state of the object.

HttpGet request is described here:

public async Task<IEnumerable<Poi>> GetPois()
        {
            var pois = await _poiService.GetPois();

            if (pois.Status == Domain.Enum.StatusCode.Ok)
            {
                return pois.Data;
            }
            else { return null; }
        }

I have an interface that describes the necessary set of manipulations with the Poi object.

IPoiService is described here:

 public interface IPoiService
    {
        Task<BaseResponse<IEnumerable<Poi>>> GetPois();

        Task<BaseResponse<Poi>> GetPoi();

        Task<BaseResponse<bool>> DeletePoi();

        Task<BaseResponse<Poi>> CreatePoi();

        Task<BaseResponse<Poi>> UpdatePoi(Poi entity);
    }

The service for working with the Poi object is described here:

public async Task<BaseResponse<IEnumerable<Poi>>> GetPois()
        {
            try
            {
                return new BaseResponse<IEnumerable<Poi>>
                {
                    Data = await _poiRepository.GetAll().ToListAsync(),
                    Status = Domain.Enum.StatusCode.Ok
                };
            }
            catch(Exception ex)
            {
                return new BaseResponse<IEnumerable<Poi>>
                {
                    Status = Domain.Enum.StatusCode.InternalServerError,
                    Description = $"[GetPois]: {ex.Message}"
                };
            }
        }

BaseResponse and the corresponding interface represents the response from the database, so it doesn't affect the update problem in any way. I also have a repository that directly implements instance operations at the database level.

The repository is described here:

public class PoiRepository : IBaseRepository<Poi>
    {
        private readonly ApplicationDbContext db;

        public PoiRepository(ApplicationDbContext db)
        {
            this.db = db;
            db.Database.OpenConnection();
        }

        public Task Create(Poi entity)
        {
            throw new NotImplementedException();
        }

        public Task Delete(Poi entity)
        {
            throw new NotImplementedException();
        }

        public IQueryable<Poi> GetAll()
        {
            return db.Pois;           
        }

        public Poi Update(Poi entity)
        {
            db.Pois.Update(entity);
            db.SaveChanges();
            
            return entity;
        }
    }

Thus, I get the problem that in order to get the current data, I need to perform two HttpGet requests, and only after that EF Core will return its current value to me.

  • How do you create instances of your service and repository? If you get it from ServiceProvider, how you add them to it? – Oliver Beck Dec 05 '22 at 21:59
  • @OliverBeck using dependency injector, which is described in program.cs of the asp .net core project – Jeffrey Willis Dec 05 '22 at 22:12
  • You add it scoped? – Oliver Beck Dec 05 '22 at 22:24
  • @OliverBeck actually yes – Jeffrey Willis Dec 05 '22 at 22:37
  • Where are you observing the stale data? within a breakpoint for the Get() call, or in your web front end? One factor to consider is whether response caching is tripping you up. Since your calls are not parameterized it may be that a GetAll type method is just serving a cached response. If the DbSet itself is serving stale data then we would need to see where you are triggering the Update. TBH code like `db.Pois.Update(entity);` scares the bejeezuz out of me, it has no place in web applications if detached entities are being passed from the web client. – Steve Py Dec 06 '22 at 00:57
  • Do you set a breakpoint to check the value of GetAll() method every time request? They return different value? – Xinran Shen Dec 06 '22 at 02:22
  • @XinranShen the GetAll() method on the first request returns an obsolete value, but when I try to get the dbset a second time, I get updated data – Jeffrey Willis Dec 06 '22 at 12:22
  • @StevePy the breakpoint is set on the GetAll() method, on the first try I get stale data, on the second it is updated. You said that the db.Pois.Update(entity); approach causes you doubts, please tell me what is wrong here and what can I change – Jeffrey Willis Dec 06 '22 at 12:24

1 Answers1

0

The reason that Update(entity) sends off warning bells is that you are passing entities between server and client and back. When a controller returns a View(entity) you are sending a reference entity to the view engine to build the view. The view's @Model allows you to apply bindings but it is not a client-side copy of the entity. However, when your form submit or Ajax call etc. calls back with the @model that is NOT an entity, let alone the entity the view engine was given. It will only be a copy of data and only as complete as the view bindings could populate.

So it's hard to deduce what exactly you are witnessing without stepping through the application, but my gut says you are most likely getting confused by what you think is passing entity references around. Think of it this way, in your POST actions you could accept a set of ints, strings, and such for each of the values of the model, or a completely different class definition (DTO/ViewModel) with the same fields as the entity. ASP.Net would attempt to fill in using the data submitted with a Form POST or Ajax call. By accepting an "Entity" you are just telling EF to populate the data into a new untracked entity class. It's not the same instance as a DbContext originally loaded, and the DbContext is a different instance (or should be) than when the entity was originally loaded, it isn't tracking the entity that was originally loaded.

The resulting object will only contain the details that the view happened to have stored in the individual bound controls, pieced back together behind the scenes.

My recommendation is simply to never pass entities to, and especially from a view. Use an explicit ViewModel to represent the state sent to and from a view, then in your Update method:

  • Fetch the actual entity using the ViewModel ID,
  • Check a concurrency token (RowVersionNumber / Timestamp) to ensure no changes were made to the DB since you originally fetched the data to populate the View. (optional, but recommended)
  • Validate the data in your view model
  • Copy the data from the view model into the Entity. (Automapper can help here)
  • SaveChanges()

No use of Update or Attach in the DbContext/DbSet.

Steve Py
  • 26,149
  • 3
  • 25
  • 43