- Home
- LLBLGen Pro
- Bugs & Issues
Blazor server, LLBLGEN and AuditorBase
Joined: 14-Feb-2017
Hi,
I use LLBLGEN in a Blazor server application and want to use the AuditorBase class to be able to log in an AUDIT table all create, update or delete operations. In the AUDIT table, I would like to put the name of the current user but can't figure how to do it in the right way.
Indeed, to retrieve the current user, I use the AuthenticationStateProvider class (provided by Blazor Framework) like this.
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var userId = authenticationState.User.Claims.Where(claim => claim.Type == "UserId").Select(claim => claim.Value).FirstOrDefault());
The AuthenticationStateProvider class is retrieved from the IOC using the [Inject] attribute on the class that need it, in my case, the class inherited from AuditorBase.
Is there a way to do it ?
Note : I already tried to create a static property in the Auditor class but have same issue than discussed in this thread : https://www.llblgen.com/tinyforum/Thread/27298/1 so this is not a way to go.
Joined: 14-Feb-2017
If you want to say something like this
[DependencyInjectionInfo(typeof(IEntity2), "AuditorToUse")]
[Serializable]
public class EntityAuditor : AuditorBase
{
private string userId;
public EntityAuditor(AuthenticationStateProvider authenticationStateProvider)
{
var authenticationState = await authenticationStateProvider.GetAuthenticationStateAsync();
this.userId = authenticationState.User.Claims.Where(claim => claim.Type == "UserId").Select(claim => claim.Value).FirstOrDefault();
}
I can't because authenticationStateProvider variable must be passed as parameter to the EntityAuditor class and if I do this, the following exception is thrown.
No parameterless constructor defined for type 'xxx.yyy.EntityAuditor'
If I add a parameterless constructor, for sure, my code will not be called.
All would be fine if the EntityAuditor would be in the same IOC than the AuthenticationStateProvider class but it isn't or I don't know how to do it
Joined: 28-Nov-2005
Hi Guilles,
In the LLBLGen's examples in Github, there is an Auditing example. The user info is resolved using a static class called SessionHelper. Then it's used in the Auditor this way:
private string GetCurrentUserID()
{
// obtain user info from Session
return SessionHelper.GetUserID();
}
Another option is to use a dependency injection framework that injects such parameters in runtime for you. I think you are using some kind of framework for that but the type is not resolved so that's why the error. We should see more of your code to see where you set the rules for type resolution, or where you are adding the trasient classes to be resolved.
Joined: 17-Aug-2003
Additionally: It might be solved if you use the default DI system of asp.net/blazor here, and not ours, for the auditor injection? See https://www.llblgen.com/Documentation/5.8/LLBLGen%20Pro%20RTF/Using%20the%20generated%20code/gencode_usingdi.htm#using-another-dependency-injection-framework
That at least gives you the Auditor to be injected by the same system as the rest. We're not familiar with blazor's specifics so I can't comment on that. The static variable route might work if you mark the variable with [ThreadStatic]
but it might still be not enough.
Joined: 14-Feb-2017
Hi,
this is exactly what I was looking for : https://www.llblgen.com/Documentation/5.8/LLBLGen%20Pro%20RTF/Using%20the%20generated%20code/gencode_usingdi.htm#using-another-dependency-injection-framework
Unfortunately, the RuntimeConfiguration.SetDependencyInjectionInfo method has a different signature than the one in the link. Indeed, it looks like this one
RuntimeConfiguration.SetDependencyInjectionInfo(
new List<Assembly>()
{
typeof(AddressEntity).Assembly,
this.GetType().Assembly
},
new List<string>()
{
"Northwind",
"Authorizers",
});
Am I missing a namespace ?
Joined: 14-Feb-2017
Since which version of the dll ? Because as you can see, in 5.7.0.0, I only have one
#region Assembly SD.LLBLGen.Pro.ORMSupportClasses, Version=5.7.0.0, Culture=neutral, PublicKeyToken=ca73b74ba4e3ff27
// C:\Users\gilles.marceau\.nuget\packages\sd.llblgen.pro.ormsupportclasses\5.7.1\lib\netstandard2.0\SD.LLBLGen.Pro.ORMSupportClasses.dll
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
namespace SD.LLBLGen.Pro.ORMSupportClasses
{
//
// Summary:
// Central configuration class to allow configuration of various runtime elements
// at the code level without the requirement of a config file.
public static class RuntimeConfiguration
{
//
// Summary:
// Gets the configuration method object for configuring Entity(Core) specific settings.
public static EntityConfigMethods Entity { get; }
//
// Summary:
// Gets the configuration method object for configuring Xml serialization specific
// settings
public static XmlConfigMethods Xml { get; }
//
// Summary:
// Gets the configuration method object for configuring TraceSwitch instances of
// the runtime.
public static TracingConfigMethods Tracing { get; }
//
// Summary:
// Gets the configuration method object for configuring Prefetch/Prefetch2 instances.
public static PrefetchPathConfigMethods Prefetching { get; }
//
// Summary:
// Adds the connectionstring specified to the configuration, under the key specified.
// If a connectionstring with the key specified is already present it is overwritten
// with the specified connectionstring
//
// Parameters:
// key:
// The key the connectionstring has to be stored under. Has to be unique
//
// connectionString:
// The connectionstring to add. Can't be empty.
public static void AddConnectionString(string key, string connectionString);
//
// Summary:
// Configures the Dynamic Query Engine which DQEConfigurationBase derived type has
// been specified using the specified configureFunc. An instance of TDQEConfig is
// passed to configureFunc after which its contents is passed to the associated
// Dynamic Query Engine.
//
// Parameters:
// configureFunc:
//
// Type parameters:
// TDQEConfig:
public static void ConfigureDQE<TDQEConfig>(Action<TDQEConfig> configureFunc) where TDQEConfig : DQEConfigurationBase;
//
// Summary:
// Gets the connectionstring added with AddConnectionString under the key specified
//
// Parameters:
// key:
// the key the connection string was added under
//
// Returns:
// the connectionstring found or empty string if not found
public static string GetConnectionString(string key);
//
// Summary:
// Sets the dependency information to use by the runtime. It will rebuild the internal
// DI store with the information provided.
//
// Parameters:
// assembliesWithInjectables:
// The assemblies to examine for types decorated with DependencyInjectionInfo attributes.
//
// namespaceFilterFragments:
// The namespace fragments to filter target types with. Can be null, in which case
// all target types specified in the found DependencyInjectionInfo attributes are
// used.
public static void SetDependencyInjectionInfo(IEnumerable<Assembly> assembliesWithInjectables, IEnumerable<string> namespaceFilterFragments);
//
// Summary:
// Class for the set methods to configure PrefetchPath/PrefetchPath2 instances in
// the runtime.
public class PrefetchPathConfigMethods
{
//
// Summary:
// Sets the PrefetchPath/PrefetchPath2 UseRootMaxLimitAndSorterInPrefetchPathSubQueries
// default value.
//
// Parameters:
// value:
public PrefetchPathConfigMethods SetUseRootMaxLimitAndSorterInPrefetchPathSubQueriesFlag(bool value);
}
//
// Summary:
// Class for the set methods to configure TraceSwitch instances in the runtime.
public class TracingConfigMethods
{
//
// Summary:
// Sets the trace level of the switch with the name specified to the level specified.
//
// Parameters:
// switchName:
// The name of the trace switch to set the level
//
// level:
// the level to set the trace switch to
public TracingConfigMethods SetTraceLevel(string switchName, TraceLevel level);
}
//
// Summary:
// Class for the set methods to configure Entity(Core) related settings
public class EntityConfigMethods
{
//
// Summary:
// Sets EntityCore.AllowReadsFromDeletedEntities
//
// Parameters:
// value:
public EntityConfigMethods SetAllowReadsFromDeletedEntities(bool value);
//
// Summary:
// Sets EntityCore.BuildInValidationBypassMode.
//
// Parameters:
// mode:
public EntityConfigMethods SetBuildInValidationBypassMode(BuildInValidationBypass mode);
//
// Summary:
// Sets EntityFieldCore.CaseSensitiveStringHashCodes
//
// Parameters:
// value:
public EntityConfigMethods SetCaseSensitiveStringHashCodes(bool value);
//
// Summary:
// Sets EntityCore.MakeSettingNonNullableFieldsToNullFatal
//
// Parameters:
// value:
public EntityConfigMethods SetMakeSettingNonNullableFieldsToNullFatal(bool value);
//
// Summary:
// Sets EntityCore.MarkSavedEntitiesAsFetched
//
// Parameters:
// value:
public EntityConfigMethods SetMarkSavedEntitiesAsFetched(bool value);
//
// Summary:
// Sets EntityCore.ScaleOverflowCorrectionActionToUse
//
// Parameters:
// action:
public EntityConfigMethods SetScaleOverflowCorrectionActionToUse(ScaleOverflowCorrectionAction action);
}
//
// Summary:
// Class for the set methods to configure Xml serialization specific settings
public class XmlConfigMethods
{
//
// Summary:
// Sets XmlHelper.CultureNameForXmlValueConversion
//
// Parameters:
// name:
public XmlConfigMethods SetCultureNameForXmlValueConversion(string name);
}
}
}
Joined: 14-Feb-2017
Joined: 14-Feb-2017
As proposed, I tried to * register the EntityAuditor class in the asp .net core IOC * use the SetDependencyInjectionInfo to set thr EntityAuditor class to LLBLGEN like this.
RuntimeConfiguration.SetDependencyInjectionInfo(app.ApplicationServices,
(s, e) => e.AuditorToUse = (IAuditor)s.GetService(typeof(EntityAuditor)));
1) EntityAuditor without AuthenticationStateProvider injected, register as singleton
public class EntityAuditor : AuditorBase
{
public EntityAuditor()
{
}
....
services.AddSingleton<EntityAuditor>();
=> the app doesn't crash but because AuthenticationStateProvider isn't injected, I can't retrieve the user
2) EntityAuditor with AuthenticationStateProvider injected, register as singleton
public class EntityAuditor : AuditorBase
{
private AuthenticationStateProvider AuthenticationStateProvider { get; set; }
public EntityAuditor(AuthenticationStateProvider authenticationStateProvider)
{
this.AuthenticationStateProvider = authenticationStateProvider;
}
....
services.AddSingleton<EntityAuditor>();
=> the app crashes because AuthenticationStateProvider is a scoped service (one for each session/user) and EntityAuditor is a singleton
Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Neptune.AEP.Web.Shared.Technical.Audit.EntityAuditor Lifetime: Singleton ImplementationType: Neptune.AEP.Web.Shared.Technical.Audit.EntityAuditor': Cannot consume scoped service 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider' from singleton 'Neptune.AEP.Web.Shared.Technical.Audit.EntityAuditor'.)'
3) EntityAuditor without AuthenticationStateProvider injected, register as scoped
public class EntityAuditor : AuditorBase
{
public EntityAuditor()
{
}
....
services.AddScoped<EntityAuditor>();
=> the app crashes when the RuntimeConfiguration.SetDependencyInjectionInfo is called
Cannot resolve scoped service 'Neptune.AEP.Web.Shared.Technical.Audit.EntityAuditor' from root provider.
Did I make a mistake ?
Joined: 21-Aug-2005
2) EntityAuditor with AuthenticationStateProvider injected, register as singleton
public class EntityAuditor : AuditorBase
{
private AuthenticationStateProvider AuthenticationStateProvider { get; set; }
public EntityAuditor(AuthenticationStateProvider authenticationStateProvider)
{
this.AuthenticationStateProvider = authenticationStateProvider;
}
....
services.AddSingleton<EntityAuditor>();
Could you please try to add the service as Transient: services.AddTransient<EntityAuditor>(); ?
Joined: 14-Feb-2017
As asked, if service is added as Transient like this
public class EntityAuditor : AuditorBase
{
private AuthenticationStateProvider AuthenticationStateProvider { get; set; }
public EntityAuditor(AuthenticationStateProvider authenticationStateProvider)
{
this.AuthenticationStateProvider = authenticationStateProvider;
}
....
services.AddTransient<EntityAuditor>();
the following error occured.
2021-07-28T10:12:04.3640343+02:00 [ERR] System.InvalidOperationException: Cannot resolve 'Neptune.AEP.Web.Shared.Technical.Audit.EntityAuditor' from root provider because it requires scoped service 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'. at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Neptune.AEP.Web.LLBLGenExtensions.<>c.<UseLLBLGen>b__0_0(IServiceProvider s, IEntityCore e) in Z:\GIT\neptune-aep\src\Neptune.AEP.Web\Startup.cs:line 114 at SD.LLBLGen.Pro.ORMSupportClasses.DependencyInjectionInfoProvider.PerformIServiceProviderBasedDI(IEntityCore injectionTarget) at SD.LLBLGen.Pro.ORMSupportClasses.DependencyInjectionInfoProviderSingleton.PerformDependencyInjection(Object injectionTarget) at SD.LLBLGen.Pro.ORMSupportClasses.EntityCore`1.PerformDependencyInjection() at Neptune.AEP.Entities.EntityClasses.IntervenantEntity.InitClassMembers() in Z:\GIT\neptune-aep\src\Neptune.AEP.Entities\DatabaseGeneric\EntityClasses\IntervenantEntity.cs:line 149 at Neptune.AEP.Entities.EntityClasses.IntervenantEntity.InitClassEmpty(IValidator validator, IEntityFields2 fields) in Z:\GIT\neptune-aep\src\Neptune.AEP.Entities\DatabaseGeneric\EntityClasses\IntervenantEntity.cs:line 163 at Neptune.AEP.Entities.EntityClasses.IntervenantEntity..ctor(IEntityFields2 fields) in Z:\GIT\neptune-aep\src\Neptune.AEP.Entities\DatabaseGeneric\EntityClasses\IntervenantEntity.cs:line 84 at Neptune.AEP.Entities.FactoryClasses.IntervenantEntityFactory.CreateImpl(IEntityFields2 fields) in Z:\GIT\neptune-aep\src\Neptune.AEP.Entities\DatabaseGeneric\FactoryClasses\EntityFactories.cs:line 206 at SD.LLBLGen.Pro.ORMSupportClasses.EntityFactoryCore2.Create(IEntityFields2 fields) at SD.LLBLGen.Pro.ORMSupportClasses.EntityFactoryCore2.Create()
Joined: 17-Aug-2003
I'm sorry this is so painful, but it appears that the ASP.NET team wants to make things as cumbersome as possible it seems.
As you register the auditor with an interface you have to add it with the interface as well:
services.AddSingleton<IAuditor, EntityAuditor>();
RuntimeConfiguration.SetDependencyInjectionInfo(app.ApplicationServices,
(s, e) => e.AuditorToUse = (IAuditor)s.GetService(typeof(EntityAuditor)));
Or AddTransient<IAuditor, EntityAuditor>()
instead of singleton if you don't want to have the instance to stick around.
That's what we use in our tests... (the host below is of course not needed in asp.net)
[OneTimeSetUp]
public void Init()
{
var host = Host.CreateDefaultBuilder().ConfigureServices((_, services) =>
services.AddSingleton<IAuditor, Auditor1>()
.AddSingleton<IMyAuditor<CustomerEntity>, Auditor2<CustomerEntity>>()
.AddTransient<IAuthorizer, Authorizer1>()
.AddTransient<IMyAuthorizer<CustomerEntity>, Authorizer2<CustomerEntity>>())
.Build();
RuntimeConfiguration.SetDependencyInjectionInfo(host.Services, (s, e)=>e.AuditorToUse=(IAuditor)s.GetService(typeof(IAuditor)),
(s, e) => e.AuthorizerToUse=(IAuthorizer)s.GetService(typeof(IMyAuthorizer<>).MakeGenericType(e.GetType())));
}
[Test]
public void InjectAuditorAndAuthorizerTest()
{
var c = new CustomerEntity();
Assert.IsNotNull(c.AuditorToUse);
Assert.IsNotNull(c.AuthorizerToUse);
Assert.IsNull(c.ConcurrencyPredicateFactoryToUse);
Assert.AreEqual(typeof(Auditor1), c.AuditorToUse.GetType());
Assert.AreEqual(typeof(Authorizer2<CustomerEntity>), c.AuthorizerToUse.GetType());
}
Joined: 14-Feb-2017
1) If I use
services.AddSingleton<IAuditor, EntityAuditor>();
and
RuntimeConfiguration.SetDependencyInjectionInfo(app.ApplicationServices,
(s, e) => e.AuditorToUse = (IAuditor)s.GetService(typeof(IAuditor)));
and I comment the AuthenticationStateProvider like this, all is fine but I can't retrieve the username
//private AuthenticationStateProvider AuthenticationStateProvider { get; set; }
//public EntityAuditor(AuthenticationStateProvider authenticationStateProvider)
//{
// this.AuthenticationStateProvider = authenticationStateProvider;
//}
public EntityAuditor()
{
}
2) If I uncomment the AuthenticationStateProvider like this,
private AuthenticationStateProvider AuthenticationStateProvider { get; set; }
public EntityAuditor(AuthenticationStateProvider authenticationStateProvider)
{
this.AuthenticationStateProvider = authenticationStateProvider;
}
the following exception is thrown (which seems normal because I try to inject a scoped service in a singleton)
System.AggregateException: 'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: SD.LLBLGen.Pro.ORMSupportClasses.IAuditor Lifetime: Singleton ImplementationType: Neptune.AEP.Web.Shared.Technical.Audit.EntityAuditor': Cannot consume scoped service 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider' from singleton 'SD.LLBLGen.Pro.ORMSupportClasses.IAuditor'.)'
3 If I let the AuthenticationStateProvider uncomment and change the AddSingleton to AddTransient, same kind of exception but now I don't really understand it
Cannot resolve 'SD.LLBLGen.Pro.ORMSupportClasses.IAuditor' from root provider because it requires scoped service 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'.
I'm a little disappointed about this error
Joined: 17-Aug-2003
Perhaps this thread gives more insight? https://stackoverflow.com/questions/48590579/cannot-resolve-scoped-service-from-root-provider-net-core-2 It looks like some nasty side effect of how ASP.NET core DI works...