MarimerLLC / cslaforum

Discussion forum for CSLA .NET
https://cslanet.com
Other
31 stars 6 forks source link

How to configure ASP .NET CORE #765

Closed skeeler88 closed 5 years ago

skeeler88 commented 5 years ago

Question First sorry that this is my second post about this topic but I am unable to figure out how to configure CSLA .net with ASP .NET Core with the following specs:

Version and Platform CSLA version: 4.11.0 OS: Windows 10 Platform: ASP.NET Core Technologies: EF Core Database: MySQL OR SqlServer Visual Studio: 2017

So I've been able to reproduce the exact circumstance I have found. This may be beneficial for anyone else looking into the latest and greatest. I have created a fresh solution for you to look at if you need the exact code let me know where to upload it.

My question is how to configure APS.NET CORE Startup.cs? When configuring the Startup.cs as such the DbContextManager has a RefCount of 2 on the second database operation in a single request. Although IMMEDIATELY after using (var....DbContextManager<>.GetManager()) the DbContext is disposed.

The culprit is services.AddHttpContextAccessor(). If I remove that line (& or the Microsoft identity framework configuration because it calls the exact line) everything works. However this item is essential for me. Or vice versa, if I remove Csla from Startup everything works. I have looked at the source and found NO interaction specifically and by all means I dont see why it doesnt work.

To retrieve the object I am using the DI dataportal in the Razor Page constructor.

What am I doing wrong? Thank you!

Csla.DataPortalException: 'DataPortal.Fetch failed (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.

 public class Startup
    {
        private static ILogger<Startup> _logger;

        public Startup(IConfiguration configuration)
        {
            Configuration = new ConfigurationBuilder()
                .AddConfiguration(configuration)
                .AddEnvironmentVariables()
                .Build()
                .ConfigureCsla();
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddDbContext<ApplicationDbContext>(options =>
            {
                if (string.Compare(Configuration["DatabaseSettings:DbProvider"], "mysql", true) == 0)
                {
                    options.UseMySql("Server=localhost;Database=Test;user=root;password=password");
                }
                else
                {
                    options.UseSqlServer("Server=localhost;Database=Test;user=root;password=password");
                }
            }, ServiceLifetime.Transient);
            services.AddIdentity<IdentityUser, IdentityRole>(setup => setup.User.RequireUniqueEmail = true)
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
            services.AddHttpContextAccessor();
            services.AddCsla();
            services.ConfigureCsla();
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }
            app.UseCsla();

            app.UseStaticFiles();

            app.UseCookiePolicy();

            loggerFactory.AddLog4Net(new Log4NetProviderOptions("./Logs/log.txt", true));
            _logger = loggerFactory.CreateLogger<Startup>();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Account}/{action=Login}");
            });
        }
    }
rockfordlhotka commented 5 years ago

Am I right in assuming everything is running on the web server, so the logical client-side and logical server-side data portal code is running on the same machine (the data portal is configured to use LocalProxy)?

And you are getting the data portal factory injected into the page - so that's all client-side stuff.

And your DataPortal_XYZ methods are just like those from, let's say, the Using CSLA 4: Data Access book? Where the root object's DP_XYZ method opens with a using statement to create the dbcontext, and all child interactions are inside that using, and each Child_XYZ method also opens with a using statement?

OH WAIT - looking at your code I see that you are NOT using the CSLA dbcontext stuff at all? You are using something else - maybe from EF Core?

            services.AddDbContext<ApplicationDbContext>(options =>
            {
                if (string.Compare(Configuration["DatabaseSettings:DbProvider"], "mysql", true) == 0)
                {
                    options.UseMySql("Server=localhost;Database=Test;user=root;password=password");
                }
                else
                {
                    options.UseSqlServer("Server=localhost;Database=Test;user=root;password=password");
                }
            }, ServiceLifetime.Transient);

I haven't used this at all, so I honestly have no idea. All my answers thus far (in the previous thread for example) were assuming the use of the CSLA dbcontext helpers...

skeeler88 commented 5 years ago

Thank you sooo much for the reply I thought I was SOS. Yes I have purchased your books and it is exactly like this, I mean I've written megabytes of CSLA.NET code in .Net Framework. I even cracked them open this morning to figure out whether I am doing something off.

All code is running Local on the machine. No remote DataPortal. The dataportal is set to "Local".

I only have ApplicationDbContext to utilize Microsoft Identity Framework. Even when I ref it purely in the Get (see below) the second call to the DbContext is gone (disposed)! I'm pulling my hair out to figure this 1 out. The remaining schema is behind the scenes (I plan to move this all behind the scenes eventually). It has to do something with DbContextManager and AddHttpContextAccessor() I think.

Two instances of TestContext get created, but immediately after the second one (the second using) it gets disposed.

I even tried reffing it directly in the OnGet().

        public void OnGet()
        {
            using (var ctx = DbContextManager<TestContext>.GetManager())
            {
               var tset1 =  (from test in ctx.DbContext.TestAbcde
                        select new TableDto()
                        {
                            Test = test.Test
                        }).First();
            }

            using (var ctx = DbContextManager<TestContext>.GetManager())
            {
                var tset1 = (from test in ctx.DbContext.TestAbcde
                             select new TableDto()
                             {
                                 Test = test.Test
                             }).First();
            }
            var test3 = DataPortal.Fetch<Table>();
            var test4 = DataPortal.Fetch<Table>();

        }
rockfordlhotka commented 5 years ago

In CSLA 5 you should be able to inject that dbcontext from the services collection into your DP_XYZ methods.

In CSLA 4 there's no support for injecting something like that into the server-side code automatically. You need to make the ServiceProvider object available to the server-side code and call GetService explicitly in your DP_XYZ method to get access to the dbcontext object.

@dazinator wrote up some good info on how he did this in https://github.com/MarimerLLC/csla/issues/787 and that might be helpful to you for CSLA 4.

The next prerelease of CSLA 5 will include support for automatically injecting services into DP_XYZ methods. You basically define parameters beyond the criteria, mark them with an attribute, and they'll be injected.

rockfordlhotka commented 5 years ago

I don't understand why you are using the CSLA dbcontext in a client-side OnGet method? It is designed for use in server-side code _inside a DP_XYZ method_.

skeeler88 commented 5 years ago

Yeah it's all garbage code that is posted because I've been trying everything so I upgraded to 5 this morning and need to port that over and try to inject that stuff in the DP_XYZ. So essentially the ASP.NET will be in Startup.cs

services.AddTransient

Then in the controller when I call the DataPortal I pass in the present DbContext.

rockfordlhotka commented 5 years ago

The current CSLA 5 prerelease does not have the DI enhancements.

skeeler88 commented 5 years ago

OK will do thanks a ton will take a peak at that thread for CSLA 4. Appreciate the time you've taken.

skeeler88 commented 5 years ago

So that approach solved nearly everything. Except FetchAsync. Internal framework null ref exception, still trying to figure out as to the why.

System.NullReferenceException HResult=0x80004003 Message=Object reference not set to an instance of an object. Source=Csla.AspNetCore.Mvc StackTrace: at Csla.Web.ApplicationContextManager.SetUser(IPrincipal principal) at Csla.Threading.ContextParams.SetThreadContext() at Csla.Threading.CslaTaskScheduler.b__40(Object ) at System.Threading.ThreadPoolWorkQueue.Dispatch()

Within the default ApplicationContextManager, the HttpContext is null which is odd to me as CSLA .NET injects the service provider which is not null.

Commenting out the Csla.ApplicationContext.ContextManager = new ApplicationContextManager(serviceProvider); at least removes the error but not sure what other implications that has.

Brannos1970 commented 5 years ago

@skeeler88 Did you get this figured out and get the login working on your project?

dazinator commented 5 years ago

Some pointers: note in the issue rocky linked above, I had to set a custom context manager that was able to resolve HttpContext using IHttpContextAccessor (which it resolved from the IServiceProvider passed in its constructor):

Csla.ApplicationContext.WebContextManager = options.WebContextManager ?? new HttpContextAccessorContextMananger(serviceProvider);
            Csla.ApplicationContext.ContextManager = new ApplicationContextManager();

This is so that when you resolve your DbContext, it ultimately should come from HttpContext.RequestServices when there is a current HttpContext and when there isnt a current HttpContext then it needs to come from your root application IServiceProvider (ideally from a new ServiceScope which would be disposed on the last deref of DbContextManager)

It looks to me like you are using a context manager that is now provided by csla itself for this purpose- it may be worth stepping through that code to see why its hitting the null ref - perhaps it doesnt handle null httpcontext and you are running some code outside of a request?

skeeler88 commented 5 years ago

@Brannos1970 Yes, the key is to do what Rocky said to follow almost what was said in the linked issue above.

There are some architectural decisions & exceptions first noted by @dazinator.

The goal was to not have the DAL (with all the Linq EF queries) referenced to the UI with the EF model; because it Startup.cs you have to add the DbContext to the Services. (& because people would just call the DAL directly from the UI potentially).

First the Entity Framework model needs to be in a separate library referenced to the ASP .NET Core UI layer & the specific DAL EF layer. The issue becomes where does the custom DbContextManager live and the custom Csla Extension methods live (in the linked article by rocky above) within the solution. We decided the custom DbContextManager lives in the specific DAL Entity Framework layer (or assembly). Then we created a separate assembly to house the Csla Extensions methods, so that the DbContextManager and the ASP .NET Core project can utilize the extensions.

In addition if you are writing unit tests, to test the CSLA Business Objects specifically & you are not specifying a database "name". You need to write some additional code to create the DbContext to test the business layer in the Csla Extensions and custom DbContextManager.

If you are working within a team, the consensus has to be solidified as to what development practices you need to follow because its a bit confusing but once you have the infrastructure setup, back to coding CSLA .NET!

Hope that helps.

note this is the n-tier architecture with a UI, Business Model, Data Access, and Data Access Entity Framework layers. with a local data portal

skeeler88 commented 5 years ago

@rockfordlhotka Hi Rocky, I was curious as to how to approach this item in CSLA 5.0? (e.g. do we pass the DbContext through the DataPortal.Fetch(params object[] params)? I'm guessing we might need to do something in the Startup.cs.

rockfordlhotka commented 5 years ago

@skeeler88 generally I recommend using the encapsulated invocation approach for data access as described in the Using CSLA 4 books.

In that model, which you can see in the current ProjectTracker, BlazorExample, MvcExample, and UnoExample samples, a data access implementation is injected as a parameter for each data access method.

Your DAL might directly create the dbcontext, or you should be able to use the .NET Core DI subsystem to create the DAL object, injecting a dbcontext into each DAL object as it is created.