-
I have a DI service which wraps an instance of my DAL db connection object. I want this wrapper to be scoped to ensure that I have only one DB connection open at a time for this particular user's call to the DataPortal. public class JTDataAdapterManager
{
private JTDataAccessAdapter _adapter; //DAL connection
private Guid guid = Guid.NewGuid();
//Use DI
public JTDataAdapterManager(ApplicationContext applicationContext, DBConfiguration dbConfigOptions)
{
//Set DAL Config options here with dbConfigOptions
...
// open connection if we have crossed the logical and/or physical dataportal so it is safe to access the database
if (applicationContext.LogicalExecutionLocation == ApplicationContext.LogicalExecutionLocations.Server)
{
_adapter = new JTDataAccessAdapter(true, connectionTimeoutInt);
}
else
{
throw new Exception("The Database can only be accessed on the server side of DataPortal");
}
} The service is defined in my server side Blazor app as "Scoped" builder.Services.AddScoped<LLBLGenDataUtilities.JTDataAdapterManager>(); I assumed since the service is defined as "Scoped" that the first call to this on the server side of the DataPortal would create a new one of these...but subsequent chained calls (still on the server side) would pick up the existing created one. Example, here is an instance of a server side [Fetch] method that gets called first: public class TranslationRecordInfoList : BusinessBaseClasses.JTReadOnlyListBase<TranslationRecordInfoList, TranslationRecordInfo>
{
...
[Fetch]
private void DP_Fetch(
LLBLGenDataUtilities.Criteria.LLBLGenFilterCriteria criteria,
[Inject] LanguageInfoListFactory languageInfoListFactory,
[Inject] TranslationRecordInfoFactory translationInfoFactory,
[Inject] LLBLGenDataUtilities.JTDataAdapterManager cxnManager)
{
RaiseListChangedEvents = false;
IsReadOnly = false;
using (EntityCollection<TranslationEntity> list = new EntityCollection<TranslationEntity>())
{
// load values
cxnManager.ActiveAdapter.FetchEntityCollection(list, criteria.Filter, criteria.MaxRecordsToReturn, criteria.SortExpression);
if (list.Count > 0)
{
LanguageInfoList languages = languageInfoListFactory.GetAll();
foreach (var language in languages)
{
foreach (var entity in list.Where(ent => ent.LanguageId == language.Id))
{
Add(TranslationRecordInfo.Load(translationInfoFactory, entity, language));
}
}
}
}
IsReadOnly = true;
RaiseListChangedEvents = true;
} This is the first method that gets called in a chain of calls...JTDataAdapterManager is injected so this is where one should get created. A few lines into that method, I fetch another business object I need LanguageInfoList languages = languageInfoListFactory.GetAll(); That call ends up here in the LanguageInfoList object public class LanguageInfoList : BusinessBaseClasses.JTReadOnlyListBase<LanguageInfoList, LanguageInfo>
{
...
[Fetch]
private void DP_Fetch(
LLBLGenDataUtilities.Criteria.LLBLGenFilterCriteria criteria,
[Inject] LanguageInfoFactory languageInfoFactory,
[Inject] LLBLGenDataUtilities.JTDataAdapterManager cxnManager)
{
RaiseListChangedEvents = false;
IsReadOnly = false; The problem is I check the "guid" property of the cxnManager in the second call and it is a new GUID (different than the one in the first fetch method. I was not expecting that. I thought once you cross the DataPortal, the LocalProxy had created a scope which would apply for DI until you end that original call into the DP. I used have this class just like Rocky's where I did ref counting and stuck an instance of it inside the ApplicationContext. That worked fine and I "could" keep doing that, but I figured I could get rid of all that extra stuff and really simplify this class by just defining it as scoped. |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 8 replies
-
I did some more poking. Turns out that in each DataPortal [Fetch] method referenced above, the injected ApplicationContext object is new each time (acting like Transient). In the first fetch method (first one across the dataportal), I put a value in LocalContext. But then, while still on the server side of dataportal, in the second fetch method, the ApplicationContext does NOT have the value in it. I'm not sure what I'm doing wrong. |
Beta Was this translation helpful? Give feedback.
-
I added the following "CreateScopePerCall = false" to the configuration and now it seems to be working a little closer to what I expected. builder.Services.AddCsla(cslaOptions =>
cslaOptions
.AddServerSideBlazor()
.DataPortal()
.AddServerSideDataPortal()
.UseLocalProxy(proxyOptions =>
proxyOptions.CreateScopePerCall = false)); Now it works like this:
@rockfordlhotka et.al. can you explain what are the implications of setting that to false? I can see it creates a new scope every call to a DataPortal method, but that doesn't explain why that was needed and why it is defaulted to true. Even with it working now with that set to false, I think I need to go back to the ref counting and Using statements, because otherwise in Blazor Server Side the database connection never closes since the code (for the circuit) is still running on the server even after the DataPortal method finishes. Well at least with a LocalDataPortal anyway. This BSS model is so tricky to get my mind wrapped around. |
Beta Was this translation helpful? Give feedback.
-
It feels like there is a second part to your question, which I thought I would help try to explain in more detail as a separate answer. Blazor Server uses scopes a bit differently than what we have seen before in ASP.NET Core, so your confusion is understandable. In any other ASP.NET Core type, a scope is created for each request to the server. For example, let's consider MVC. When an HTTP request to the MVC application is received from the client machine, a scope is created, the MVC controller instance is created in that scope, as are any constructor parameters that the DI container creates for you, then the action method is executed. That scope completes after the MVC controller's action method completes, when the MVC controller is disposed. So here a scope means "per request" In Blazor Server, there are no HTTP requests - not once the app is up and running anyway. Instead, a permanent connection is open between the client and the server. The scope was created when the connection was set up, and remains for the lifetime of that connection. That means, in Blazor Server, a scope is "per user". It is exactly this difference that has caused Rocky's huge workload to change Csla for version 6. Scopes work differently in Blazor Server, and what is more because they work differently, they get used for things that they didn't get used for in the past. This includes holding, and providing access to, the user's security context information. [In other Blazor hosting models, there is only one user for the appdomain/process. Therefore, you can think of ASP.NET's scopes being "per user" in all of the Blazor hosting models, but we notice the difference most in Blazor Server, because it's the only hosting model so far in which there can be multiple users accessing the same hosting process/appdomain.] What the CreateScopePerCall setting does is ask the implementer of IDataPortal to create a scope at the start of the main data access operation - from/inside of whatever scope is currently active - so that you get a "per request" type behaviour during that data access operation. At the moment, it appears that if you start two data access operations by using two IDataPortal instances then you have two different scopes. What you are seeing inside of your application that uses a master "unit of work" inside of Blazor Server suggests that there are probably three nested scopes:
Anything created in layer 3 only lasts as long as that scope, which is the length of time of the data access operation of that layer. Thus, LanguageInfoList and TranslationRecordInfoList appear to be inside of different scopes, but Rocky's answer says to me that he intends that layer 3 should never be created. It looks like you have probably uncovered a bug, but you have also highlighted that we need to be sure to make people aware that Csla creates scopes. I didn't know that until recently, so I got some unexpected behaviour myself for the same sort of reason. I hope the detail helps, if not you then maybe someone else! |
Beta Was this translation helpful? Give feedback.
I added the following "CreateScopePerCall = false" to the configuration and now it seems to be working a little closer to what I expected.
Now it works like this:
@rockfordlhotka et.al. can you explain what ar…