Blazor server, LLBLGEN and AuditorBase

Posts   
 
    
Posts: 62
Joined: 14-Feb-2017
# Posted on: 19-Jul-2021 16:47:13   

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.

Walaa avatar
Walaa
Support Team
Posts: 14983
Joined: 21-Aug-2005
# Posted on: 20-Jul-2021 04:42:09   

Why not using the above couple lines of code into the Auditor CTor to set a member variable?

Posts: 62
Joined: 14-Feb-2017
# Posted on: 20-Jul-2021 11:35:18   

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

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 21-Jul-2021 09:13:55   

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.

David Elizondo | LLBLGen Support Team
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 21-Jul-2021 09:16:41   

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.

Frans Bouma | Lead developer LLBLGen Pro
Posts: 62
Joined: 14-Feb-2017
# Posted on: 21-Jul-2021 15:17:09   

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 ?

Walaa avatar
Walaa
Support Team
Posts: 14983
Joined: 21-Aug-2005
# Posted on: 22-Jul-2021 04:21:11   

Both overloads exist in the same namespace: SD.LLBLGen.Pro.ORMSupportClasses

Posts: 62
Joined: 14-Feb-2017
# Posted on: 22-Jul-2021 10:12:49   

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);
        }
    }
}
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 22-Jul-2021 11:23:56   

It was introduced in 5.8.

Frans Bouma | Lead developer LLBLGen Pro
Posts: 62
Joined: 14-Feb-2017
# Posted on: 27-Jul-2021 10:26:01   

OK, thanks

Posts: 62
Joined: 14-Feb-2017
# Posted on: 27-Jul-2021 15:28:59   

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 ?

Walaa avatar
Walaa
Support Team
Posts: 14983
Joined: 21-Aug-2005
# Posted on: 27-Jul-2021 20:51:47   
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>(); ?

Posts: 62
Joined: 14-Feb-2017
# Posted on: 28-Jul-2021 10:11:40   

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()
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 28-Jul-2021 13:07:49   

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());
}
Frans Bouma | Lead developer LLBLGen Pro
Posts: 62
Joined: 14-Feb-2017
# Posted on: 30-Jul-2021 10:02:44   

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 simple_smile

        //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

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 30-Jul-2021 10:13:18   

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...

Frans Bouma | Lead developer LLBLGen Pro