0

I have an app that collects data and writes it to a database. The database type is not known in advance, it's defined via an .ini file. So I have a method like this, if the database is Firebird SQL:

public bool writeToDB()
{
    FbConnection dbConn = new FbConnection(connString);
    dbConn.Open();
    FbTransaction dbTrans = dbConn.BeginTransaction();
    FbCommand writeCmd = new FbCommand(cmdText, dbConn, dbTrans);

    /* some stuff */

    writeCmd.ExecuteNonQuery();
    dbTrans.Commit();
    writeCmd.Dispose();
    dbConn.Close();
    return true;
}

To make the same work for e.g. MS Access database, I only have to replace FbConnection, FbTransaction and FbCommand with OleDbConnection, OleDbTransaction and OleDbCommand respectively.

But I don't want to have a separate identical method for each type of database. Is it possible to define the database connection / transaction / command type at runtime, after the database type is known?

Thanks

  • 3
    You need a [factory method](https://en.wikipedia.org/wiki/Factory_method_pattern). – Ian Kemp Apr 09 '21 at 14:13
  • 1
    I'd personally probably define an interface to the database and implement each specific database type (i.e. SQL Server, MS Access, etc.) in it's own class library. At runtime, use the settings in the ini file to bind dynamically to the correct implementation and just consume it via the defined interfaces. – Martin Apr 09 '21 at 14:13
  • Are you programmatically determining the database type at runtime? If you're determining it before run-time you could just put a database type flag in a config file and read it at run time. – Mikael Apr 09 '21 at 14:15
  • *To make the same work for e.g. MS Access database* - don't do it; by all means have a stab at working with multiple DBs that are good at following the SQL standards, but throwing Access into the mix will make life too much like hard work. Be prepared for a long battle too - EF has a team of people working on getting it right for many years already – Caius Jard Apr 09 '21 at 14:20
  • Am I programmatically determining the database type at runtime? I don't know if it's Yes or No :). Each user may use their preferred database type - so they define that in an .ini file (along with a connection string etc.). The app reads the .ini file once at startup. But the database type is not known at compile time. I can have a separate method for each DB type, it's like that now, but maintaining several identical methods is a PITA, prone to errors and somehow not elegant. – user3061666 Apr 09 '21 at 14:39
  • Go with @Martin's advice. Define an interface to abstract the database access and implement you logic using this interface. Then implement this interface for any db provider you want. – Artur Apr 09 '21 at 14:41
  • Side Note : get RID of the bool return. https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/exception-throwing. ❌ DO NOT return error codes. (or boolean status flags) – granadaCoder Apr 09 '21 at 18:04

2 Answers2

2

When you're writing code at this level - opening and closing connections, creating and executing commands - there's probably no benefit in trying to make this method or class database-agnostic. This code is about implementation details so it makes sense that it would be specific to an implementation like a particular database.

But I don't want to have a separate identical method for each type of database.

You're almost certainly better off having separate code for separate implementations. If you try to write code that accommodates multiple implementations it will be complicated. Then another implementation (database) comes along which almost but doesn't quite fit the pattern you've created and you have to make it even more complicated to fit that one in.

I don't know the specifics of what you're building, but "what if I need a different database" is usually a "what if" that never happens. When we try to write one piece of code that satisfies requirements we don't actually have, it becomes complex and brittle. Then real requirements come along and they're harder to meet because our code is tied in knots to do things it doesn't really need to do.

That doesn't mean that all of our code should be coupled to a specific implementation, like a database. We just have to find a level of abstraction that's good enough. Does our application need to interact with a database to save and retrieve data? A common abstraction for that is a repository. In C# we could define an interface like this:

public interface IFooRepository
{
    Task<Foo> GetFoo(Guid fooId);
    Task Save(Foo foo);
}

Then we can create separate implementations for different databases if and when we need them. Code that depends on IFooRepository won't be coupled to any of those implementations, and those implementations won't be coupled to each other.

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
  • Agreed. To OP, if you do NOT do this method.. you most certainly have to add some hack-crap code in like "if mydbtarget=="Jet") {} if mydbtarget="SqlServer" {} . This method is MUCH cleaner. – granadaCoder Apr 09 '21 at 18:03
  • 1
    Note, I said "Jet Database". MS Access is a program..on top of a Jet-Database (super technically). Don't believe me? :). https://www.nuget.org/packages/EntityFrameworkCore.Jet/ – granadaCoder Apr 09 '21 at 18:05
0

First (and Second and Third). STOP REINVENTING THE WHEEL.

https://learn.microsoft.com/en-us/ef/core/providers/?tabs=dotnet-core-cli

Alot of code and an alot of better testing has already been done.

Guess what is in that larger list:

FirebirdSql.EntityFrameworkCore.Firebird Firebird 3.0 onwards

EntityFrameworkCore.Jet Microsoft Access files

......

So I'm gonna suggest something in lines with everyone else. BUT also... allows for some reuse.

I am basing this .. on the fact the Entity Framework Core...provides functionality to several RDBMS.

See:

https://learn.microsoft.com/en-us/ef/core/providers/?tabs=dotnet-core-cli

public interface IEmployeeDomainDataLayer
{
    Task<Employee> GetSingle(Guid empKey);
    Task Save(Employee emp);
}


public abstract class EmployeeEntityFrameworkDomainDataLayerBase : IEmployeeDomainDataLayer
{
     /* you'll inject a MyDbContext into this class */

    //implement Task<Employee> GetSingle(Guid empKey); /* but also allow this to be overrideable */
    //implement Task Save(Employee emp); /* but also allow this to be overrideable */
}

public class EmployeeJetEntityFrameworkDomainDataLayer : EmployeeEntityFrameworkDomainDataLayerBase, IEmployeeDomainDataLayer
{

    /* do not do any overriding OR override if you get into a jam */
}


public class EmployeeSqlServerEntityFrameworkDomainDataLayer : EmployeeEntityFrameworkDomainDataLayerBase, IEmployeeDomainDataLayer
{

    /* do not do any overriding OR override if you get into a jam */
}

You "code to an interface, not an implementation". Aka, your business layer codes to IEmployeeDomainDataLayer.

This gives you most code in EmployeeEntityFrameworkDomainDataLayerBase. BUT if any of the concretes give you trouble, you have a way to code something up ONLY FOR THAT CONCRETE.

If you want DESIGN TIME "picking of the RDBMS", then you do this:

You inject one of the concretes ( EmployeeJetEntityFrameworkDomainDataLayer OR EmployeeSqlServerEntityFrameworkDomainDataLayer ) into your IOC, based on which backend you want to wire to.

If you want RUN-TIME "picking of the RDMBS", you can define a "factory".

public static class HardCodedEmployeeDomainDataLayerFactory
{
    public static IEmployeeDomainDataLayer getAnIEmployeeDomainDataLayer(string key)
        {
              return new EmployeeJetEntityFrameworkDomainDataLayer();
              // OR (based on key)
              return new  EmployeeSqlServerEntityFrameworkDomainDataLayer();
        }

}

The factory above suffers from IOC anemia. Aka, if your concretes need items for their constructors..you have to fudge them.

A better idea of the above is the kissing cousin of "Factory" pattern, called the Strategy Design.

It is a "kinda factory", BUT you inject the possible results of the "factory" in via a constructor. Aka, the "factory" is NOT hard coded...and does NOT suffer from IOC anemia.

See my answer here:

Using a Strategy and Factory Pattern with Dependency Injection

granadaCoder
  • 26,328
  • 10
  • 113
  • 146