json-api-dotnet / JsonApiDotNetCore

A framework for building JSON:API compliant REST APIs using ASP.NET and Entity Framework Core.
https://www.jsonapi.net
MIT License
681 stars 159 forks source link

Multiple DbContexts seems to conflict each other at runtime #1533

Closed Feraccia29 closed 6 months ago

Feraccia29 commented 6 months ago

SUMMARY

I have this project in .net 6 and I have to use two dbContexts to refer two different database. When I add them to jsonapi one is working well, while the other send this error at runtime (for example when calling a GET):

"Cannot create a DbSet for 'Model' because this type is not included in the model for the context."

If I invert the order in the list of dbTypes that I pass to AddJsonApi, then the one that previously worked now launch the error at runtime and the other works fine. So I can exclude that there's something wrong with dbcontexts or entities.

DETAILS

I'm using reflection cause it's a framework code loading plugins, but it seems to me that the reflection is working good and the error occours only at runtime. I'll provide my plugin's dbContext below. If you change the OrderBy with OrderByDescending you can see the error will change dbContext.

STEPS TO REPRODUCE

Here the code in my startup.cs:

var customDbContexts = ReflectionHelper.ResolveAll<IJsonApiDbContext>().OrderBy(x => x.GetType().Name).ToList();

List<Type> types = new List<Type>();

foreach (var customDbContext in customDbContexts)
{
    Type dbContextType = customDbContext.GetType();
    types.Add(dbContextType);

    MethodInfo method = typeof(EntityFrameworkServiceCollectionExtensions)
                        .GetMethod("AddDbContext", 1, new[] { typeof(IServiceCollection), typeof(Action<DbContextOptionsBuilder>), typeof(ServiceLifetime), typeof(ServiceLifetime) });

    MethodInfo generic = method.MakeGenericMethod(dbContextType);

    if (ConfigurationHelper.AppConfig.GetSection($"OtherDbContexts:{dbContextType.Name}").Value == "Sql")
    {
        generic.Invoke(null, new object[] { services, new Action<DbContextOptionsBuilder>(options =>
        {
            options.UseSqlServer(ConfigurationHelper.AppConfig.GetConnectionString(dbContextType.Name));
        }), ServiceLifetime.Transient, ServiceLifetime.Scoped });
    }
    else if (ConfigurationHelper.AppConfig.GetSection($"OtherDbContexts:{dbContextType.Name}").Value == "MySql")
    {
        generic.Invoke(null, new object[] { services, new Action<DbContextOptionsBuilder>(options =>
        {
            options.UseMySQL(ConfigurationHelper.AppConfig.GetConnectionString(dbContextType.Name));
        }), ServiceLifetime.Transient, ServiceLifetime.Scoped });
    }
    else
    {
        continue;
    }
}

services.AddJsonApi(options =>
{
    options.Namespace = "api/v{version}";
    options.UseRelativeLinks = true;
    options.IncludeTotalResourceCount = true;
    options.SerializerOptions.WriteIndented = true;
    options.ClientIdGeneration = ClientIdGenerationMode.Allowed;
    options.AllowUnknownQueryStringParameters = true;
    options.DefaultPageSize = null;
    options.IncludeExceptionStackTraceInErrors = true;
    options.SerializerOptions.Converters.Add(new JsonDateTimeOffsetConverter());
    options.SerializerOptions.Converters.Add(new JsonDateTimeConverter());
},
discovery =>
{
    discovery.AddCurrentAssembly();
},
dbContextTypes: types);

Here my Interface that I use just to search them in plugins' assembly

using System;
using System.Collections.Generic;
using System.Text;

namespace MIT.Fwk.Core.Documents.Interfaces
{
    public interface IJsonApiDbContext
    {

    }
}

Here my two dbContexts:

First

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using MIT.Fwk.Core.Documents.Interfaces;
using MIT.Fwk.Core.Helpers;

namespace MIT.Custom.MaeWay.Timesheet.EntityLayer
{
    public class TimesheetDbContext : DbContext, IJsonApiDbContext
    {
        public TimesheetDbContext() : this(new DbContextOptionsBuilder<DbContext>()
          .UseSqlServer(ConfigurationHelper.AppConfig.GetConnectionString("TimesheetDbContext"))
          .Options)
        {
        }

        public TimesheetDbContext(DbContextOptions<DbContext> options) : base(options)
        {
        }

        public DbSet<Tb000Company> Tb000Companies => Set<Tb000Company>();
    }
}

Second

using Microsoft.EntityFrameworkCore;
using MIT.Custom.MaeWay.Domain.EntityLayer;
using MIT.Fwk.Core.Documents.Interfaces;
using MIT.Fwk.Core.Helpers;
using MIT.Fwk.Infra.Identity.Data;

namespace MIT.Custom.MaeWay.Domain
{
    public class CustomDbContext : DbContext, IJsonApiDbContext
    {
        public CustomDbContext() : this(new DbContextOptionsBuilder<DbContext>()
          .UseSqlServer(ConfigurationHelper.AppConfig.GetConnectionString("CustomDbContext"))
          .Options)
        {
        }

        public CustomDbContext(DbContextOptions<DbContext> options) : base(options)
        {
        }

        public DbSet<CustomerTenantMap> CustomerTenantMaps => Set<CustomerTenantMap>();
    }
}

And the entities in the contexts ahead: First

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;

namespace MIT.Custom.MaeWay.Timesheet.EntityLayer
{
    [Resource]
    [Table("tb000_azienda")]
    public class Tb000Company : Identifiable<int>
    {
        [Key]
        [Column("tb000_id")]
        public override int Id { get; set; }

        [Attr]
        [Column("tb000_nome_azienda")]
        public string Tb000NomeAzienda { get; set; }

        [Attr]
        [Column("tb000_codice_azienda")]
        public string Tb000CodiceAzienda { get; set; }

        [Attr]
        [Column("tb000_partita_iva_cod_fiscale")]
        public string Tb000PartitaIvaCodFiscale { get; set; }

        [Attr]
        [Column("tb000_IsForCommessa")]
        public bool Tb000IsForCommessa { get; set; }

        [Attr]
        [Column("tb000_is_maestrale_group")]
        public bool? Tb000IsMaestraleGroup { get; set; }
    }
}

Second

using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;

namespace MIT.Custom.MaeWay.Domain.EntityLayer
{
    [Resource]
    public class CustomerTenantMap : Identifiable<int>
    {
        [Attr]public int TenantId { get; set; }
        [Attr]public int CustomerIdTimesheet { get; set; }
    }
}

SQL Code to recreate tables in db:

CREATE TABLE [dbo].[tb000_azienda](
    [tb000_id] [int] IDENTITY(1,1) NOT NULL,
    [tb000_nome_azienda] [nvarchar](255) NOT NULL,
    [tb000_codice_azienda] [nvarchar](255) NOT NULL,
    [tb000_partita_iva_cod_fiscale] [nvarchar](255) NOT NULL,
    [tb000_IsForCommessa] [bit] NOT NULL,
    [tb000_is_maestrale_group] [bit] NULL,
 CONSTRAINT [PK_tb000_azienda_tb000_id] PRIMARY KEY CLUSTERED 
(
    [tb000_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[tb000_azienda] ADD  DEFAULT ((0)) FOR [tb000_IsForCommessa]
GO

ALTER TABLE [dbo].[tb000_azienda] ADD  CONSTRAINT [DF_tb000_azienda_tb000_is_maestrale_group]  DEFAULT ((0)) FOR [tb000_is_maestrale_group]
GO

CREATE TABLE [dbo].[CustomerTenantMaps](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [TenantId] [int] NOT NULL,
    [CustomerIdTimesheet] [int] NOT NULL,
 CONSTRAINT [PK_CustomerTenantMap] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[CustomerTenantMaps]  WITH CHECK ADD  CONSTRAINT [FK__CustomerTenantMap__Tenants] FOREIGN KEY([TenantId])
REFERENCES [dbo].[Tenants] ([Id])
GO

ALTER TABLE [dbo].[CustomerTenantMaps] CHECK CONSTRAINT [FK__CustomerTenantMap__Tenants]
GO

VERSIONS USED

bkoelman commented 6 months ago

First of all, I can't observe what you do because the posted code is not runnable. Second, I don't think this is related to JADNC, because the error comes from EF Core itself. Google for it and you'll find various matches. Third, why would you pass ServiceLifetime.Transient, ServiceLifetime.Scoped? Fourth, even if this would work in EF Core, you can't just pass a set of DbContext types and assume JADNC figures out what belongs where. You need to decide per repository which DbContext it should use by injecting the appropriate types and register things appropriately. See the MultiDbContext example.

bkoelman commented 6 months ago

Closing due to a lack of response. Please provide more information if you'd like this to be reopened.