BlueshiftSoftware / EntityFrameworkCore

NoSQL providers for EntityFramework Core
Other
281 stars 57 forks source link

Add support for mongoDb database name info passed on connection string/url #15

Closed dannotsys closed 7 years ago

dannotsys commented 7 years ago

A nice feature would be using the database name information passed on the connection string/url (and be able to change dynamically on the context method, OnModelCreating - like on sql server), instead using the MongoDatabase Attribute. I have been working on a project involving ETL and tried achieve that by some other way but no success. There is any workaround on the actual source?

crhairr commented 7 years ago

The database attribute convention sets a model annotation. You can find the annotations here, which can be used from OnModelCreating like this:

public void OnModelCreating(ModelBuilder modelBuilder)
{
    var annotations = modelBuilder.MongoDb();
    annotations.Database = "DatabaseName";
    //apply other settings
}

There's also a convenience method that you can use:

public void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ForMongoDbFromDatabase("DatabaseName");
}
crhairr commented 7 years ago

As for the connection string: I do not parse it, I just pass it directly to the MongoDB C# driver. A single mongo db connection can open an arbitrary number of databases, so the database name in the connection string is largely ignored. The driver only uses the /database portion as a reference to the database that contains authentication information if the connection string contains credentials. The only time this is different is when the db is "admin", which is used to access db admin commands such as shutting down the database (see the unit tests).

I would prefer not to change the semantics of what a MongoDb connection string means. So for now, please use the attribute or model annotation.

dannotsys commented 7 years ago

Sorry Chris, my mistake about the connection string. About the approach using Annotations on OnModelCreating, I have already tried that, but EF seems to cache the database name among the model info and I was unable to change anymore, once setted. It is there some way to change it at anytime (more than once) like calling GetDatabase() on the c# driver?

crhairr commented 7 years ago

Two things..,

1) I forgot about a third option for setting the database, which is in MongoDbContextOptionsBuilder and can be used when setting up the db context:

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseMongoDb(
            "mongodb://localhost",
            options => options.UseDatabase("Database"));
    }

2) Are you also setting the [Database] attribute on the db context class? The convention for interpreting this attribute is an IModelBuiltConvention which is run after the rest of the model has been finalized, after both OnConfiguring and OnModelCreating have been called, which means that the attribute can override anything you're setting in those methods. Please make sure that you're not setting the attribute if you're trying to use direct configuration.

dannotsys commented 7 years ago

I have tried the MongoDbContextOptionsBuilder.UseDatabase approach but seems not to be working at all, the database ended up created with my DbContext class name (ContextMongoDb : DbContext). Subsequent instantiateds DbContexts (on the same process) keeped using this database name.

I am not using the Database attribute.

crhairr commented 7 years ago

Yeah, there was a bug that I noticed right after I posted my last comment. It should be fixed now, and there's a new package out there. My apologies for that.

This might be a limitation of EF Core in general. After doing a little bit of research, it appears that the IModel instances are cached as singletons based on the DbContext type. If you place a breakpoint in your OnModelCreating, you'll probably find that it's only being called once per process, despite multiples instantiations.

EF Core assumes a 1:1 relationship between a DbContext and a particular database, and EFCore-MongoDb doesn't behave any differently. I assume that users will create a different DbContext class for each specific named MongoDB database instance with which they want to communicate.

Since you're creating an ETL process, you could also create two derived DbContext types as for example SourceDbContext and the TargetDbContext and set the appropriate database names in each context's OnModelCreating method. That would be a cleaner approach from a coding perspective, as it would give a contextual usage to each DbContext.

If you really need to dynamically set the database, you can add a property or extension method on your DbContext:

    public string Database
    {
        get => Model.MongoDb().Database;
        set => Model.MongoDb().Database = value;
    }

However, this is not a supported use and I'm not sure how it would affect the runtime.

crhairr commented 7 years ago

To clarify my last comment, the IModel instances aren't cached per process. They're cached per service container. Each time you new up an IServiceCollection and register EF Core or EFCore-MongoDb dependencies, you'll get a fresh instance of the model. However, building the model is a very expensive process that involves heavy use of reflection, so this is not something that should be done dynamically.

So if this is, for example, part of a web application, you should in theory only build the model once per application startup. In that scenario, trying to change the model's database will have serious consequences, and could result in unintended data leaking not only from the source to the target (or vice versa depending on how you're setting the database name) but also between different sets of source and target databases.

If you really, absolutely need to be able to set truly dynamic database names, then your best bet will be to wrap up your ETL related DbContexts into an isolated service container that is instantiated and populated on demand by the parent application. In this scenario, it still would be wisest to have separate Source and Target contexts that are derived from the same abstract base context. This way, you'll ensure that you won't run into any issues with unintended data leaking between the source and target, and especially between different sets of source and target databases.

dannotsys commented 7 years ago

Sorry for the late feedback, worked flawless as you have pointed out.

I have created a new instance of Microsoft.Extensions.DependencyInjection.ServiceCollection on a facade class that receives a delegate (Func<IServiceCollection, IServiceCollection>) (for multi provider support) where I have pointed to MongoDbEfServiceCollectionExtensions.AddEntityFrameworkMongoDb. I have also overriden the OnConfiguring method like below from a parent abstract DbContext class for automatically set the new serviceProvider (created on my facade class too using the new serviceCollection)

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);

            optionsBuilder
                .UseInternalServiceProvider(myCustomServiceProvider);
        }

I needed this ability to change the Database at runtime for the same Entity Model due the multi tenancy requirements of my project.