I am listing data with Blazor server side and MudBlazor.
I have a user list:
UserList.razor
public partial class UserList
{
private async Task<TableData<User>> ServerReload(TableState state)
{
var admTableData = await _userService.GetUsersAsTableDataAsync(state.ToAdmTableState());
return admTableData.ToTableData();
}
}
The service for the user list looks like this:
UserService.cs
public class UserService
{
public UserService(MyDbContext myDbContext)
{
_userRepository = new UserRepository(myDbContext);
}
public Task<AdmTableData<User>> GetUsersAsTableDataAsync(AdmTableState admTableState)
{
var queryable = _userRepository.GetUsersAsQueryable();
if (!string.IsNullOrEmpty(admTableState.SearchString))
{
queryable = queryable.Where(u => u.Name.Contains(admTableState.SearchString, StringComparison.OrdinalIgnoreCase));
}
switch (admTableState.SortLabel)
{
case "Name":
queryable = queryable.OrderByDirection(admTableState.SortDirection, o => o.Name);
break;
}
return PaginationHelper.GetTableDataAsync(queryable, admTableState);
}
}
The pagination helper:
PaginationHelper.cs
public static async Task<AdmTableData<T>> GetTableDataAsync<T>(IQueryable<T> queryable, AdmTableState admTableState)
{
var admTableData = new AdmTableData<T>();
admTableData.TotalItems = await queryable.CountAsync();
admTableData.Items = await queryable.Skip(admTableState.PageNumber * admTableState.PageSize)
.Take(admTableState.PageSize).ToListAsync();
return admTableData;
}
Lastly. I am registering the services in the following way:
Program.cs
builder.Services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("myConnectionString")));
builder.Services.AddScoped<IUserService, UserService>();
If I order a column. I get this error:
Error: System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
If I do a search. It never gets the data and it keeps loading:
Loading forever screenshot
You must use IDbContextFactory if you are using Blazor server-side, because you can't use the same dbcontext instance multiple times in multiple threads.
Your dbcontext service is scoped, which means it will create a new instance while the new request to the server, but the Blazor server is a single page application and you have a single request and single dbcontext instance, and if you use the same dbcontext like a normal asp.net core application it will give you this error:
Error: System.InvalidOperationException: A second operation was started on this....
You must create dbcontext instances manually. Register your dbcontext like this:
builder.Services.AddDbContextFactory<MyDbContext>(
options => options.UseSqlServer(builder.Configuration.GetConnectionString("myConnectionString")));
and use it in your code like this:
private readonly IDbContextFactory<MyDbContext> _contextFactory;
public MyController(IDbContextFactory<MyDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public void DoSomething()
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
You can read more in DbContext Lifetime, Configuration, and Initialization.
Related
I have a class that derives from BackgroundService (IHostedService) for running background tasks. This will be added to my services using builder.Services.AddHostedService<BackgroundTaskService>()
BackgroundService's task runs for the entire duration of the web application, checking for queued data to process.
My question is, how do I instantiate an instance of DbContext from this code?
I could have the BackgroundTaskService constructor accept a DbContext. But wouldn't that keep the DbContext open forever?
And how else could I instantiate it without duplicating all the code to scan my settings file for the connection string, etc.?
The recemmended approach is to inject IDbContextFactory<TContext> as described in the following article: Using a DbContext factory (e.g. for Blazor)
Some application types (e.g. ASP.NET Core Blazor) use dependency injection but do not create a service scope that aligns with the desired DbContext lifetime. Even where such an alignment does exist, the application may need to perform multiple units-of-work within this scope. For example, multiple units-of-work within a single HTTP request.
In these cases, AddDbContextFactory can be used to register a factory for creation of DbContext instances.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextFactory<ApplicationDbContext>(
options =>
options.UseSqlServer(#"Server=(localdb)\mssqllocaldb;Database=Test"));
}
Then in your controller:
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public void DoSomething()
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
You can use scope service factory. Check here for reference.
Here you have an example:
// Injection
public class DataApi : BackgroundService
{
private readonly ILogger<DataApi> logger;
private readonly IServiceScopeFactory scopeFactory;
public DataApi(ILogger<DataApi> _logger, IConfiguration _cfg, IServiceScopeFactory _sSF)
{
logger = _logger;
scopeFactory = _sSF;
// e.g. data from appsettings.json
// var recovery = _cfg["Api:Recovery"];
}
// ...
// Usage
protected async Task DataCollector()
{
logger.LogInformation("Collector");
using (var scope = scopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
var myList = await db.MyEntity
.AsNoTracking()
.Where(t => t.active)
.ToListAsync();
if (myList.Count == 0)
{
logger.LogInformation("Empty...");
return;
}
// logic...
}
await Task.CompletedTask;
}
I have problem with ef core. I have two services which read data from database. On one page is call first service and on second page is called second service. When i click to button for create a new program i got error. I call it normally from page with inject service. Can anybody help me with it?
Show in application
builder.Services.AddDbContextPool<Context>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Connection"));
});
TestService1:
public class TestService1 : ITestService1
{
private readonly Context _context;
private readonly IMapper _mapper;
public TestService1(Context context, IMapper mapper)
{
_kreativgangContext = kreativgangContext;
_mapper = mapper;
}
public virtual async Task<AllProgramViewModel> HandleAsync(AllProgramFilterViewModel filter)
{
var model = new AllProgramViewModel();
var data = _context.Programs.Where(x => (EF.Functions.Like(x.Name ?? "", "%" + filter.Name + "%") || string.IsNullOrEmpty(filter.Name)))
.Select(x => new Core.Models.Program() { ID = x.ID, Name = x.Name, Order = x.Order });
result.Model.TotalCount = await data.CountAsync();
result.Model.Items = data.Select(x => _mapper.Map<AllProgramItemViewModel>(x));
return model;
}
}
public interface ITestService1
{
public Task<AllProgramViewModel> HandleAsync(AllProgramFilterViewModel filter);
}
Test service 2:
public class TestService2 : ITestService2
{
private readonly Context _context;
public TestService2(Context context)
{
_context = context;
}
public virtual async Task<NewProgramViewModel> HandleAsync()
{
var model = new NewProgramViewModel();
List<ProgramOrderViewModel> items = _context.Programs.Select(x => new Core.Models.Program() { Order = x.Order, ID = x.ID })
.Select(x => new ProgramOrderViewModel()
{
ID = x.ID,
Order = x.Order
}).ToList();
return await Task.FromResult(model);
}
}
public interface ITestService2
{
public Task<NewProgramViewModel> HandleAsync();
}
Error:
Error: System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Infrastructure.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at Mitar.Kreativgang.Admin.Handlers.TestService2.HandleAsync() in D:\Programming\Kreativgang\Src\Mitar.Kreativgang.Admin\Handlers\TestService2.cs:line 26
at Mitar.Kreativgang.Admin.Pages.Program.ProgramNew.OnInitializedAsync() in D:\Programming\Kreativgang\Src\Mitar.Kreativgang.Admin\Pages\Program\ProgramNew.razor:line 114
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
This is a known and documented pitfall, explained in ASP.NET Core Blazor Server with Entity Framework Core (EFCore). In Blazor Server, the DI scope is the user circuit - essentially the user session. That means that a scoped service like TestService2 or a DbContext will remain in memory for a long time and end up reused by multiple methods and actions.
As the docs explain :
Blazor Server is a stateful app framework. The app maintains an ongoing connection to the server, and the user's state is held in the server's memory in a circuit. One example of user state is data held in dependency injection (DI) service instances that are scoped to the circuit. The unique application model that Blazor Server provides requires a special approach to use Entity Framework Core.
You need to register and use a DbContextFactory (or PooledDbContextFactory) instead of a DbContextPool, and create a new DbContext instance right where it's used.
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlServer(...));
or
builder.Services.AddPooledDbContextFactory<ContactContext>(opt =>
opt.UseSqlServer(...));
The service constructors should accept the factory instead of a context :
public TestService2(AddDbContextFactory<ContactContext> factory)
{
_factory = factory;
}
public virtual async Task<NewProgramViewModel> HandleAsync()
{
using var context=_factory.CreateContext())
{
...
}
}
Component Scope
To limit a DbContext's scope to a single component it's not enough to just inject the DbContextFactory. The DbContext instance needs to be explicitly disposed when the user navigates away from the component. To do this, the component needs to implement IDisposable. This is explained in the section Scope to the component lifetime
#implements IDisposable
#inject IDbContextFactory<ContactContext> DbFactory
...
#code
{
ContactContext? Context;
public void Dispose()
{
Context?.Dispose();
}
protected override async Task OnInitializedAsync()
{
Context = DbFactory.CreateDbContext();
...
}
}
I have this list of names (List<string>) and want to map them to entities from the database. If there is an entity with the given name, just return it. If not, create and return it.
public async Task<List<Element>> MapStringsToEntities(List<strings> raws)
{
var result = new List<Elements>();
foreach (var raw in raws)
{
var element = await _context.Elements
.Where(t => t.Name.ToLower().Equals(raw))
.SingleOrDefaultAsync();
if (element == null)
{
element = new Element(raw);
await _context.Elements.AddAsync(raw);
}
result.Add(element);
}
await _context.SaveChangesAsync();
return result;
}
Although I'm awaiting the call to SingleOrDefaultAsync() I still get an error:
InvalidOperationException: A second operation was started on this context before a previous operation completed.
This is usually caused by different threads concurrently using the same instance of DbContext.
The class where this method lives has a field of type MyAppContext:
private readonly MyAppContext _context;
And a constructor:
public ElementsService(MuAppContext ctx)
{
this._context = ctx;
}
In Startup.cs, the context is initialized like so:
public void ConfigureServices(IServiceCollection services)
{
services
.AddDbContext<MyAppContext>(options =>
options.UseSqlServer(Configuration["ConnectionString:DbString"])
);
}
Any ideas?
You only get an IEnumerable or IQueryable (or something) back from the .Where and therefore actual execution is still delayed until you ask for it to be enumerated. You need to materialise the result by calling .ToList() (or whatever) to force it to run the query and load the results into memory before you try to use the connection again.
I have a hosted service that processes objects PUT/POST via an API endpoint, that is, as soon as a new entity is given or an existing one is edited, (a) the hosted service starts processing it (a long running process), and (b) the received/modified object returned (as a JSON object) to the API caller.
When PUT/POST an entity, I see run-time errors here and there (e.g., at object JSON serializer) complaining for different issues, such as:
ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
or:
InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext.
Initially I was using a database context pool, but according to this, it seems the pooling has known issues with hosted services. Therefore, I switched to regular AddDbContext; however, neither that has solved the problem.
This is how I define the database context and the hosted service:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCustomDbContext(Configuration);
// This is the hosted service:
services.AddHostedService<MyHostedService>();
}
}
public static class CustomExtensionMethods
{
public static IServiceCollection AddCustomDbContext(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext<MyContext>(
options =>
{
options
.UseLazyLoadingProxies(true)
.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
sqlServerOptionsAction: sqlOptions => { sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); });
});
return services;
}
}
and I access the database context in hosted service as the following (as recommended here):
using(var scope = Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MyContext>();
}
Edit 1
As mentioned, the errors happen all around the code; however, since I mentioned the errors occurring on the serializer, I am sharing the serializer code in the following:
public class MyJsonConverter : JsonConverter
{
private readonly Dictionary<string, string> _propertyMappings;
public MyJsonConverter()
{
_propertyMappings = new Dictionary<string, string>
{
{"id", nameof(MyType.ID)},
{"name", nameof(MyType.Name)}
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject obj = new JObject();
Type type = value.GetType();
foreach (PropertyInfo prop in type.GetProperties())
{
if (prop.CanRead)
{
// The above linked errors happen here.
object propVal = prop.GetValue(value, null);
if (propVal != null)
obj.Add(prop.Name, JToken.FromObject(propVal, serializer));
}
}
obj.WriteTo(writer);
}
}
Update 2
An example API endpoint is as the following:
[Route("api/v1/[controller]")]
[ApiController]
public class MyTypeController : ControllerBase
{
private readonly MyContext _context;
private MyHostedService _service;
public MyTypeController (
MyContext context,
MyHostedService service)
{
_context = context;
_service = service
}
[HttpGet("{id}")]
public async Task<ActionResult<IEnumerable<MyType>>> GetMyType(int id)
{
return await _context.MyTypes.FindAsync(id);
}
[HttpPost]
public async Task<ActionResult<MyType>> PostMyType(MyType myType)
{
myType.Status = State.Queued;
_context.MyTypes.Add(myType);
_context.MyTypes.SaveChangesAsync().ConfigureAwait(false);
// the object is queued in the hosted service for execution.
_service.Enqueue(myType);
return CreatedAtAction("GetMyType", new { id = myType.ID }, myType);
}
}
The following lines are most likely causing the ObjectDisposedException error:
return await _context.MyTypes.FindAsync(id);
and
return CreatedAtAction("GetMyType", new { id = myType.ID }, myType);
This is because you are relying on this variable:
private readonly MyContext _context;
Since object myType has been attached to that context.
As I mentioned before, it is not a good idea to send context entities for serialization because by the time the serializer has a chance to fire, the context might have been disposed. Use a model (meaning a class in the Models folder) instead and map all the relevant properties from your real entity to it. for instance, you could create a class called MyTypeViewModel that contains only the properties that you need to return:
public class MyTypeViewModel
{
public MyTypeViewModel(MyType obj)
{
Map(obj);
}
public int ID { get; set; }
private void Map(MyType obj)
{
this.ID = obj.ID;
}
}
Then instead of returning the entity, use the view model:
var model = new MyTypeViewModel(myType);
return CreatedAtAction("GetMyType", new { id = myType.ID }, model);
As far as the InvalidOperationException, my educated guess is that since you are not awaiting the SaveChangesAsync method, the serializer is firing while the original operation is still in progress, causing a double hit to the context, resulting in the error.
Using await on the SaveChangesAsync method should fix that, but you still need to stop sending lazy-loaded entities for serialization.
Upon further review, the service itself might also be causing issues since you are passing it a reference to object myType:
_service.Enqueue(myType);
The same two issues may occur if the service is doing something with the object that causes a call to a now-disposed context or at the same time as other asynchronous parts (e.g. serialization) attempt to lazy-load stuff.
I cannot seem to find an answer to this question.
So in the frontend when the user loads a page we call an API for each item on that page (10 items). So that equals 10 API calls.
Most of the calls work but there are always a few that fail when trying to query the database resulting in the following error:
InvalidOperationException: A second operation started on this
context before a previous operation completed. Any instance members
are not guaranteed to be thread safe.
Now I understand that Entity Framework is not thread safe but I am unsure how to get around this error.
Everywhere where I am using a DBContext it is always injected in using the built in .net core Ioc container.
Here is the DI setup
services.AddScoped<IOmbiContext, OmbiContext>();
services.AddTransient<ISettingsRepository, SettingsJsonRepository>();
All of my repositories are setup in a Transient scope with the Context as Scoped according to this article: https://learn.microsoft.com/en-us/aspnet/core/data/entity-framework-6
Now I have tried changing the context to Transient and it still happens.
How can I avoid this?
More Information
The API Method:
[HttpGet("movie/info/{theMovieDbId}")]
public async Task<SearchMovieViewModel> GetExtraMovieInfo(int theMovieDbId)
{
return await MovieEngine.LookupImdbInformation(theMovieDbId);
}
Which eventually calls the following where the exception is being thrown:
public async Task<RuleResult> Execute(SearchViewModel obj)
{
var item = await PlexContentRepository.Get(obj.CustomId); <-- Here
if (item != null)
{
obj.Available = true;
obj.PlexUrl = item.Url;
obj.Quality = item.Quality;
}
return Success();
}
PlexContentRepository
public PlexContentRepository(IOmbiContext db)
{
Db = db;
}
private IOmbiContext Db { get; }
public async Task<PlexContent> Get(string providerId)
{
return await Db.PlexContent.FirstOrDefaultAsync(x => x.ProviderId == providerId); <-- Here
}
If you use Entity Framework Core usually you do not need to add your Database Context as an additional service
I recommend to setup your DbContext in the Startup.cs as following:
services.AddEntityFrameworkSqlServer()
.AddDbContext<OmbiContext>();
Followed by a Controller class for your API calls taking the DBContext as constructor parameter.
public class ApiController : Controller
{
protected OmbiContext ctx;
public ApiController(OmbiContext dbctx)
{
ctx = dbctx;
}
public async Task<IActionResult> yourAsyncAction()
{
// access ctx here
}
}