Already an open DataReader in Blazor server app using DI or ConnectionManager or Both
bboney opened this issue · comments
Question
I created a new Blazor Server app using an existing CSLA business and data access layer. (CSLA 5.1.0 - Local Data Portal). The app started simple, a couple of Razor pages showing read only information. (ReadOnlyList). The data on these 2 simple pages are populated in the OnParametersSetAsync method and a private Task linked to a button.
protected override async Task OnParametersSetAsync()
{
Statuses = await SalesOrderStatusList.GetSalesOrderStatusListAsync();
SalesOrderTypes = await SalesOrderTypeList.GetSalesOrderTypeListAsync();
}
private async Task GetSalesOrderDataAsync()
{
try
{
IsBusy = true;
DivisionSalesOrders = await SalesOrderDivisionSummaryList.GetSalesOrderDivisionSummaryListAsync(
CreatedOnDate,
DueDate,
(SelectedSalesOrderStatusValue == 0) ? null : (Int16?)SelectedSalesOrderStatusValue,
(SelectedSalesOrderTypeValue == 0 ? null : (Int16?)SelectedSalesOrderTypeValue));
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
finally
{
IsBusy = false;
}
}
The DAL looks like this:
public System.Data.IDataReader FetchList()
{
using (var ctx = ConnectionManager<SqlConnection>.GetManager(ConfigurationManager.ConnectionStrings["DbConn"].ConnectionString, false))
{
var cm = ctx.Connection.CreateCommand();
cm.CommandType = System.Data.CommandType.StoredProcedure;
cm.CommandText = "spsoSalesOrderStatusListSelect";
var result = cm.ExecuteReader();
return result;
}
}
The List BO Fetch looks like this:
[Fetch]
private void DataPortal_Fetch([Inject] IDalManager dalManager, [Inject] ISalesOrderStatusDal dal)
{
var rlce = RaiseListChangedEvents;
RaiseListChangedEvents = false;
IsReadOnly = false;
using (dalManager)
{
using (var data = new Csla.Data.SafeDataReader(dal.FetchList()))
while (data.Read())
{
var item = DataPortal.FetchChild<SalesOrderStatusInfo>(data);
Add(item);
}
}
IsReadOnly = true;
RaiseListChangedEvents = rlce;
}
Everything was working well, I could move between my simple pages and data loaded exactly as expected.
Then I added another ReadOnlyList to the OnParametersSetAsync of the NavMenu Razor component. The one that came in the VS project template.
protected override async Task OnParametersSetAsync()
{
// Get UserKey from Claims and Get NavigationList
if (Csla.ApplicationContext.User != null)
{
if(Csla.ApplicationContext.User.Identity is ClaimsIdentity)
{
ClaimsIdentity id = (ClaimsIdentity)Csla.ApplicationContext.User.Identity;
Claim userKeyClaim = id.FindFirst("UserKey");
if (userKeyClaim != null)
{
int userKey = Convert.ToInt32(userKeyClaim.Value);
navigations = await UserNavigationList.GetUserNavigationListAsync(NavigationTypeValues.BlazorNavigationType, userKey);
}
}
}
}
After adding this code in the NavMenu, I get the following error when starting the app on the Status list (GetSalesOrderStatusListAsync) in the Razor page:
'There is already an open DataReader associated with this Command which must be closed first.'
Based on this, I assume the ConnectionManager in the DAL is giving me a Connection that is still in use on another query (Busy). Since I get this error on one of the Page objects (Statuses), I assume the NavMenu (Navigations) is using the connection. If I comment out the navigation code in the NavMenu, the code runs fine. If I comment out the 2 lists in the razor page, the navigation list runs fine. It is only when they both run that we receive this error.
BTW, the DalManager and DAL classes are added using services.AddTransient() in StartUp.
I went a couple of directions to try to resolve this issue.
-
I got rid of the IDataReader returning to the BO Fetch and returned DTO's instead. I thought closing the reader as early as possible would help. This did not help. Same error.
-
Got rid of DalManager and ConnectionManager, added SqlConnection to DI using AddTransient(). Then I added a SqlConnection property to my DAL classes and a public constructor to set the SqlConnection. (I think Rocky did something like this in his Blazor book?) Still Injecting the DAL class into the BO Fetch. Slightly different error message about "Object Not Set..." on SqlConnection_getType. Keep in mind, if I still comment out one or the other Lists in Blazor, this code works fine.
So, to me, it seems like AddTransient is not working for me, at least not for getting a new SqlConnection. BTW, I turned off Pooling too on SqlConnection.
Here is what I did get working:
Still injecting DAL classes, but not injecting or using DalManager, ConnectionManager. And not injecting SqlConnection.
Change in DAL:
public List<SalesOrderStatusDto> FetchList()
{
List<SalesOrderStatusDto> list = new List<SalesOrderStatusDto>();
using (var cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DbConn"].ConnectionString))
{
cn.Open();
SqlCommand cm = cn.CreateCommand();
cm.CommandType = System.Data.CommandType.StoredProcedure;
cm.CommandText = "spsoSalesOrderStatusListSelect";
using (SafeSqlDataReader data = new SafeSqlDataReader(cm.ExecuteReader()))
{
while (data.Read())
{
SalesOrderStatusDto dto = new SalesOrderStatusDto()
{
SalesOrderStatusKey = data.GetInt32("SalesOrderStatusKey"),
CreatedByUserId = data.GetString("CreatedByUserId"),
CreatedByUserKey = data.GetInt32("CreatedByUserKey"),
CreateOnDate = data.GetDateTime("CreatedOnDate"),
LastChanged = (byte[])data.GetValue("LastChanged"),
ModifiedByUserId = data.GetString("ModifiedByUserId"),
ModifiedByUserKey = data.GetInt32("ModifiedByUserKey"),
ModifiedOnDate = data.GetDateTime("ModifiedOnDate"),
Name = data.GetString("Name"),
SalesOrderStatusId = data.GetString("SalesOrderStatusId"),
SalesOrderStatusValue = data.GetInt16("SalesOrderStatusValue")
};
list.Add(dto);
}
}
}
return list;
}
So, my questions are:
- What are anyone's results using ADO.NET in AspNetCore 3.1/Blazor, Dependency Injection and CSLA?
- Is the DalManager concept still valid?
- Is the ConnectionManager concept still valid? Trying to reuse Open connections?
- Am I completely missing something stupid, like usual?
Version and Platform
CSLA version: 5.1.0
OS: Windows 10 - Dev Machine
Platform: ASP.NET Core 3.1, Blazor Server
SqlClient: Microsoft.Data.SqlClient v 1.1.1
If you use the connection manager things need to be nested within the using
blocks at each level.
It seems like you might be getting the connection manager instance in your DAL, but also making more than one DAL call - so the two DAL calls wouldn't be inside the same using
block right?
Also, in your description of the problem you are showing UI code from Blazor pages.
IT IS SUPER CRITICAL to remember that CSLA has logical client-side and server-side code, even when using the local data portal.
You can't flow things like database contexts or pretty much anything except the business object graph from client to server and back.
In other words, your UI code should be immaterial, as all that matters is that it calls something like
var result = await DataPortal.FetchAsync<MyType>(criteria);
From your perspective that bridges to the server-side code decorated with the Fetch
attribute. That is the first place you can start doing anything like getting database connections, contexts, etc.
You can think of that Fetch
method as being the root of a "transaction", or one atomic set of server-side interactions. Once that method returns the server is "gone" and control returns to the client.
Thanks Rocky for your replies.
I included the Blazor stuff because these same BO and DAL classes have been in use in an ASP.NET Core Web API solution for a couple of years. We recently upgraded the DAL and BO's to use CSLA 5.1 and DI in the DataPortal methods. That upgrade has caused NO SqlConnection problems in the Web API solution. We also upgraded our Web API to .NET Core 3.1 and still NO Issues. We actually saw some improvements in performance!
But when we tried to use the same DAL and BO classes in Blazor Server, we ran into the SQLConnection issue. We are not calling our Web API from the Blazor Server solution, just trying to use the BO's directly. Like God meant for us to... :)
I did some more testing this morning. This is the DAL code that always works in Blazor Server when there are DataPortal calls from 2 Razor components (NavMenu and my Landing Page) (in OnParametersSetAsync):
public List<SalesOrderStatusDto> FetchList()
{
List<SalesOrderStatusDto> list = new List<SalesOrderStatusDto>();
using (var cn = new SqlConnection(ConfigurationManager.ConnectionStrings["dbConn"].ConnectionString))
{
cn.Open();
using (var cm = (SqlCommand)cn.CreateCommand())
{
cm.CommandType = System.Data.CommandType.StoredProcedure;
cm.CommandText = "spsoSalesOrderStatusListSelect";
Trace.WriteLine("Connection in SalesOrderStatusDal: " + cm.Connection.ClientConnectionId);
using (SafeSqlDataReader data = new SafeSqlDataReader(cm.ExecuteReader()))
{
while (data.Read())
{
SalesOrderStatusDto dto = new SalesOrderStatusDto()
{
SalesOrderStatusKey = data.GetInt32("SalesOrderStatusKey"),
CreatedByUserId = data.GetString("CreatedByUserId"),
CreatedByUserKey = data.GetInt32("CreatedByUserKey"),
CreateOnDate = data.GetDateTime("CreatedOnDate"),
LastChanged = (byte[])data.GetValue("LastChanged"),
ModifiedByUserId = data.GetString("ModifiedByUserId"),
ModifiedByUserKey = data.GetInt32("ModifiedByUserKey"),
ModifiedOnDate = data.GetDateTime("ModifiedOnDate"),
Name = data.GetString("Name"),
SalesOrderStatusId = data.GetString("SalesOrderStatusId"),
SalesOrderStatusValue = data.GetInt16("SalesOrderStatusValue")
};
list.Add(dto);
}
}
}
}
return list;
}
This DAL code (using ConnectionManager) does not work:
public List<SalesOrderStatusDto> FetchList()
{
List<SalesOrderStatusDto> list = new List<SalesOrderStatusDto>();
using (var ctx = ConnectionManager.GetManager("dbConn", true))
{
using (var cm = (SqlCommand)ctx.Connection.CreateCommand())
{
cm.CommandType = System.Data.CommandType.StoredProcedure;
cm.CommandText = "spsoSalesOrderStatusListSelect";
Trace.WriteLine("Connection in SalesOrderStatusDal: " + cm.Connection.ClientConnectionId);
using (SafeSqlDataReader data = new SafeSqlDataReader(cm.ExecuteReader()))
{
while (data.Read())
{
SalesOrderStatusDto dto = new SalesOrderStatusDto()
{
SalesOrderStatusKey = data.GetInt32("SalesOrderStatusKey"),
CreatedByUserId = data.GetString("CreatedByUserId"),
CreatedByUserKey = data.GetInt32("CreatedByUserKey"),
CreateOnDate = data.GetDateTime("CreatedOnDate"),
LastChanged = (byte[])data.GetValue("LastChanged"),
ModifiedByUserId = data.GetString("ModifiedByUserId"),
ModifiedByUserKey = data.GetInt32("ModifiedByUserKey"),
ModifiedOnDate = data.GetDateTime("ModifiedOnDate"),
Name = data.GetString("Name"),
SalesOrderStatusId = data.GetString("SalesOrderStatusId"),
SalesOrderStatusValue = data.GetInt16("SalesOrderStatusValue")
};
list.Add(dto);
}
}
}
}
return list;
}
We get this error on the cm.ExecuteReader():
System.InvalidOperationException: 'There is already an open DataReader associated with this Connection which must be closed first.'
Also, here is the DataPortal_Fetch in the BO's:
[Fetch]
private void DataPortal_Fetch([Inject] ISalesOrderStatusDal dal)
{
var rlce = RaiseListChangedEvents;
RaiseListChangedEvents = false;
IsReadOnly = false;
var data = dal.FetchList();
foreach (var dto in data)
{
var item = DataPortal.FetchChild<SalesOrderStatusInfo>(dto);
Add(item);
}
IsReadOnly = true;
RaiseListChangedEvents = rlce;
}
I added a Trace to show the ConnectionId in the DAL's just before the ExecuteReader and here are the results using ConnectionManager:
Connection in NavigationDal: e2dd59f3-0718-40f6-a3bd-adbfc99b5398
Connection in SalesOrderStatusDal: e2dd59f3-0718-40f6-a3bd-adbfc99b5398
Exception thrown: 'System.InvalidOperationException' in Microsoft.Data.SqlClient.dll
An exception of type 'System.InvalidOperationException' occurred in Microsoft.Data.SqlClient.dll but was not handled in user code
There is already an open DataReader associated with this Connection which must be closed first.
Notice that the Connections are the same. As I understand ConnectionManager, it does want to "share" open database connections for use when doing a Transaction. Do we just, not need it, for non-transaction DataPortal calls?
When I use SqlConnection instead of ConnectionManager I get this trace result and no error:
Connection in NavigationDal: 3bff3957-7b7f-45d6-8e84-0a7931f98517
Connection in SalesOrderStatusDal: 34bba6fd-bf39-4184-9ad4-3f59f3669570
Connection in SalesOrderTypeDal: 34bba6fd-bf39-4184-9ad4-3f59f3669570
I assume SqlConnection Pooling is giving me the same connection for the last 2. Those last 2 are run, one after the other using await.
I am not saying there is something in Csla.Data.SqlClient.ConnectionManager that is wrong, I am just trying to find the best way to use ADO.NET in CSLA/Blazor Server and ConnectionManager.
It seems like ConnectionManager is giving me Connections that are already in use, even when used in separate DAL classes and separate DataPortal calls. Does that make sense?
This is all very strange, to be sure.
I must say, that overall I'd recommend switching away from the CSLA connection manager types in any case, because you can/should be injecting the SQL connection object via DI and relying on DI scoping to make it all work.
Your DAL class should be able to be injected, and when the DI container creates your DAL instance, it can inject a SqlConnection into your DAL as it is created.
We had error results in implementing SqlConnection injection into DAL classes also. Again, it works fine in ASPNET Core Web API app, but not in Blazor Server app when 2 Blazor components "run at the same time".
We don't think this is a CSLA issue but something odd in Blazor Server and the AspNetCore 3.1 DI container combination. We also agree that the SqlConnection should be injected into the DAL for ADO.NET DAL's.
We are injecting our DAL into our DataPortal Methods (Fetch, etc.)
DAL Injection:
[Fetch]
private void DataPortal_Fetch([Inject] ISalesOrderStatusDal dal)
{
var rlce = RaiseListChangedEvents;
RaiseListChangedEvents = false;
IsReadOnly = false;
var data = dal.FetchList();
foreach (var dto in data)
{
var item = DataPortal.FetchChild<SalesOrderStatusInfo>(dto);
Add(item);
}
IsReadOnly = true;
RaiseListChangedEvents = rlce;
}
We tested injecting Microsoft.Data.SqlClient.SqlConnection into our DAL (As Rocky suggested above) and had a different error message but we think the cause is the same. We don't know the exact cause however.
public class SalesOrderStatusDal : ISalesOrderStatusDal
{
public SqlConnection cn { get; set; }
public SalesOrderStatusDal(SqlConnection sqlConnection)
{
this.cn = sqlConnection;
}
public List<SalesOrderStatusDto> FetchList()
{
List<SalesOrderStatusDto> list = new List<SalesOrderStatusDto>();
using (cn)
{
cn.Open();
using (var cm = (SqlCommand)cn.CreateCommand())
{
cm.CommandType = System.Data.CommandType.StoredProcedure;
cm.CommandText = "spsoSalesOrderStatusListSelect";
Trace.WriteLine("Connection in SalesOrderStatusDal: " + cm.Connection.ClientConnectionId);
using (SafeSqlDataReader data = new SafeSqlDataReader(cm.ExecuteReader()))
{
while (data.Read())
{
SalesOrderStatusDto dto = new SalesOrderStatusDto()
{
SalesOrderStatusKey = data.GetInt32("SalesOrderStatusKey"),
CreatedByUserId = data.GetString("CreatedByUserId"),
CreatedByUserKey = data.GetInt32("CreatedByUserKey"),
CreateOnDate = data.GetDateTime("CreatedOnDate"),
LastChanged = (byte[])data.GetValue("LastChanged"),
ModifiedByUserId = data.GetString("ModifiedByUserId"),
ModifiedByUserKey = data.GetInt32("ModifiedByUserKey"),
ModifiedOnDate = data.GetDateTime("ModifiedOnDate"),
Name = data.GetString("Name"),
SalesOrderStatusId = data.GetString("SalesOrderStatusId"),
SalesOrderStatusValue = data.GetInt16("SalesOrderStatusValue")
};
list.Add(dto);
}
}
}
}
return list;
}
}
All Services (DAL & SqlConnection) are added using AddTransient in Startup.cs: (That scan extension is from Scrutor, sweet extension BTW, registers all DAL classes with one statement.)
string cn = ConfigurationManager.ConnectionStrings["dnConn"].ConnectionString;
services.AddTransient<SqlConnection>(e => new SqlConnection(cn));
services.Scan(scan => scan
.FromAssemblyOf<DalManager>()
.AddClasses()
.AsMatchingInterface()
.WithTransientLifetime());
This is the error we receive on the cm.ExecuteReader in the DAL:
System.NullReferenceException: 'Object reference not set to an instance of an object.'
My question:
How can this be? We should get a new SqlConnection for every instance of our DAL. And a new instance of this DAL for every DataPortal call. Right? Something is weird with the DI in Blazor Server or in our setup.
Does this happen if you refresh the same data twice on the same page? You'd think so, because that'd reuse the already-open connection right?
I'm having a hard time replicating your issue, so I'm trying to figure out the difference.
Let me show what I have so we can maybe figure out the difference?
Startup config:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddTransient<IValueDal, ValueDal>();
services.AddTransient((provider) =>
{
var constring = "Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename=E:\\src\\BlazorApp2\\BlazorApp2\\TestData.mdf;Integrated Security=True;Connect Timeout=30";
var result = ConnectionManager<SqlConnection>.GetManager(constring, false);
return result;
});
services.AddCsla();
}
DAL:
using System.Collections.Generic;
using Csla.Data;
using Microsoft.Data.SqlClient;
namespace BlazorApp2.Data
{
public interface IValueDal
{
List<string> GetValues();
}
public class ValueDal : IValueDal
{
private ConnectionManager<SqlConnection> conn;
public ValueDal(ConnectionManager<SqlConnection> connection)
{
conn = connection;
}
public List<string> GetValues()
{
var result = new List<string>();
using (conn)
{
using var cmd = conn.Connection.CreateCommand();
cmd.CommandText = "SELECT * FROM [dbo].[Values]";
using var reader = cmd.ExecuteReader();
while (reader.Read())
result.Add(reader.GetString(1));
}
return result;
}
}
}
Business class:
using System;
using Csla;
namespace BlazorApp2.Data
{
[Serializable]
public class ValueList : ReadOnlyListBase<ValueList, ValueInfo>
{
[Fetch]
private void Fetch([Inject] IValueDal dal)
{
RaiseListChangedEvents = false;
IsReadOnly = false;
foreach (var item in dal.GetValues())
Add(DataPortal.FetchChild<ValueInfo>(item));
IsReadOnly = true;
RaiseListChangedEvents = true;
}
}
[Serializable]
public class ValueInfo : ReadOnlyBase<ValueInfo>
{
public static readonly PropertyInfo<string> ValueProperty = RegisterProperty<string>(nameof(Value));
public string Value
{
get => GetProperty(ValueProperty);
private set => LoadProperty(ValueProperty, value);
}
[FetchChild]
private void FetchChild(string value)
{
Value = value;
}
}
}
UI code that retrieves the list twice, which should reuse the connection right?
@page "/"
<h1>List of values</h1>
<div><input type="button" @onclick="GetData" value="Get data" /></div>
@if (Data != null)
{
<div>
@foreach (var item in Data)
{
<span>@item.Value<br /></span>
}
</div>
}
@if (Data2 != null)
{
<div>
@foreach (var item in Data2)
{
<span>@item.Value<br /></span>
}
</div>
}
@code
{
private BlazorApp2.Data.ValueList Data;
private BlazorApp2.Data.ValueList Data2;
private void GetData()
{
Data = Csla.DataPortal.Fetch<BlazorApp2.Data.ValueList>();
Data2 = Csla.DataPortal.Fetch<BlazorApp2.Data.ValueList>();
}
}
Rocky,
I just pushed a repo to CslaSampleBlazor.
The master branch is working without using ConnectionManager. It is New-ing up SQLConnection in DAL methods.
The using-connection-manager branch has some strange behavior. I setup ConnectionManager as use suggested above. (Only in the DAL Methods this sample uses)
With ConnectionManager, I get a 30 second delay in the app starting OR I get a ConnectionBroken error. If you comment out the data call in NavMenu, app loads fine. OR, if you comment out the data call in Index, app loads fine.
I am going to add a 3rd branch that uses SQLConnection injection into the DAL, instead of ConnectionManager and see what happens.
Please take a look if you can. Database is in the repo too.
Thanks,
Ben
I added another branch to the repo mentioned above that uses SqlConnection injection into the DAL. I get this error on DataReader.Execute every time: Invalid operation. The connection is closed.
I seems that the DI in Blazor server is not giving new instances of SqlConnection.
Branch is: using-sqlconnection-inject
Anyone else see this?
Ben
The big difference I see between my code and yours is that I'm injecting the ConnectionManager
into the DAL type, rather than creating it inside the DAL method, though I don't know if that's a big deal.
I looked at your SQL connection approach, and I think you need to use AddScoped
instead of AddTransient
. The data portal creates a new scope for each call to the server-side code (so for each root operation). I would think you'd normally want one connection for a root and all its children so you can, for example, maintain a transaction across all of those related operations.
Also, you can always change the way you declare the creation of the connection in Startup
so you can put a breakpoint on that line - then you can see whether or not a new connection is being created.
services.AddScoped((provider) =>
{
// create connection here
});