Click here to Skip to main content
15,890,882 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I am building a Web API to interface with a database. The target database can be via one of three servers, and this option can change from one call to the next.

I am wondering what is the ideal technique for handling these options that will be presented to the API on each call.

What I have tried:

Here is an example of a method in one of my controllers:

C#
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id, string server)
{
    if(id < 0)
        return BadRequest();

    string connectionString = _config.GetConnectionString(server);

    if (string.IsNullOrEmpty(connectionString))
        return BadRequest();

    var op = await _prodData.GetOperatorByID(id, connectionString);
           
    if(op is null || op.Count == 0)
        return NotFound();

    if(op.Count > 1)
        throw new Exception($"Too many records returned.  Expected 1 record, received {op.Count} records.");

            
    return Ok(op);
}

You can see that the way I propose to handle the server option is to pass it in as a parameter in the API call.

Ultimately this is what will have to happen because the API cannot know from one call to the next which server it will be asked to target.

But a consequence of this is that I end up with the same piece of code being repeated for every method inside every controller:
C#
string connectionString = _config.GetConnectionString(server);

if (string.IsNullOrEmpty(connectionString))
        return BadRequest();

Since I am completely new to developing APIs, I am concerned that there may some better technique that I am not aware of.

The constructor for a controller is injected with an IProductionData:
C#
public OperatorController(IConfiguration config, IProductionData prodData)
{
    _prodData = prodData;
    _config = config;
}

I did wonder about some scheme where I could have multiple instances of this IProductionData in my configuration setup, each with its own connection string already setup, and then the choice of server would trigger a decision for which IProductionInstance to use. But that seemed messier than the simpler solution I have above.

Anyway, I would appreciate any advice on technique from more the more experience API programmers.
Posted
Updated 10-Mar-21 6:07am
v2
Comments
[no name] 8-Mar-21 14:36pm    
Maybe the "data access layer" should do the connecting.

When you say "interface with a database", are you talking about actually accessing the database, via stored procs or queries? If so, that's called a DAL (Data Access Layer).

The DAL usually contains methods that simply calls the database.It doesn't know anythign about your entities, or even the specific database it's using.

On top of the DAL, you have a BLL (Business Logic Layer). This object contains methods that use methods in the DAL. Each BLL method accepts a parameter that specifies data to be inserted/updated into the database, and/or returns data from the database.

As far as connection strings go, they can live either in the web.config file, or can be hard-coded in the code.

I have a couple of articles on CodeProject that provide a generic DAL, as well as a connection string manager. Since the BLL would be application specific, I can't really provide anything that could help you, other than point you at other articles I've written that use some form of the DAL article.

Generic DAL for ADO.Net - A Revisit[^]

A Connection String Manager for Multi-Environment Ecosystems[^]


Here's an article that uses the code in the DAL article, and that implements a BLL object so you have an example.

Entity Factory - Get Your ORM-less Freak On![^]
 
Share this answer
 
Comments
bh_ 9-Mar-21 8:41am    
Thanks for your reply and your time.

So, I do have what you describe as a DAL - this is a class library that I wrote to handle the actual interaction with the database.

But, since this class library does not have access to BLL's config files (appsettings.json in my case) then I need a way to send in the required connection string to the DAL from the BLL.

And that's what I'm doing here - the user makes their API query and specifies the server that will be used. Then the BLL uses this decision to extract a connection string from appsettings.json and it sends it to the DAL which is responsible for doing the actual work with the database.

Does this sound correct to you? Well... I suppose it *is* correct in that it definitely works. But would your appraisal of that be good or bad?
#realJSOP 9-Mar-21 9:07am    
I have no idea how your code is architected and cannot recommend anything specific. In my environment, we keep all of our connection strings in the code (they're built dynamically according to the database server they need to use) as opposed to being in config files, and our DAL/BLL code is built to use them that way. The articles I cited are essentially the way we do it, and were provided as an example.

Without digging into your code, I'm honestly not really qualified to tell you if I think there might be a better way. What I have found is that the act of documenting code often reveals weaknesses that exist in your thought processes. I've refactored a lot of code as a result of trying to explain it in a document.
How about a custom model binder:
C#
[ModelBinder(typeof(ServerConnectionModelBinder))]
public class ServerConnection
{
    public ServerConnection(string connectionString)
    {
        if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException(nameof(connectionString));
        ConnectionString = connectionString;
    }
    
    public string ConnectionString { get; }
}
C#
public class ServerConnectionModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(ServerConnection)) return false;
        
        ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (val is null) return false;
        
        var server = val.RawValue as string;
        if (server is null)
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type");
            return false;
        }
        
        var config = (IConfiguration)actionContext.Request.GetDependencyScope().GetService(typeof(IConfiguration));
        if (config is null) 
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Configuration service not registered");
            return false;
        }
        
        string connectionString = config.GetConnectionString(server);
        if (string.IsNullOrEmpty(connectionString))
        {
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"Server '{server}' was not found.");
            return false;
        }
        
        bindingContext.Model = new ServerConnection(connectionString);
        return true;
    }
}
C#
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id, ServerConnection server)
{
    if (id < 0) return BadRequest();
    if (server is null) return BadRequest();
    
    var op = await _prodData.GetOperatorByID(id, server.ConnectionString);
    ...


If you don't want to check for null in every action, maybe a global action filter could help:
C#
public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, new
            {
                Message = "The data provided is not valid.",
                ModelErrors = modelState
                    .Where(p => p.Value.Errors.Count != 0)
                    .ToDictionary(p => p.Key, p => p.Value.Select(e => new
                    {
                        Message = e.Exception is null ? e.ErrorMessage : e.Exception.Message,
                        StackTrace = e.Exception?.StackTrace,
                    })),
            });
        }
    }
}
C#
public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        GlobalConfiguration.Configuration.Filters.Add(new ValidateModelAttribute());
        ...
    }
}
C#
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id, ServerConnection server)
{
    if (id < 0) return BadRequest();
    
    var op = await _prodData.GetOperatorByID(id, server.ConnectionString);
    ...
 
Share this answer
 

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900