Access to DbContext with dependency injection

25,382

Solution 1

Building upon your self answer.

Refactor MyClass to be dependent on abstractions and not too tightly coupled to concretions.

Here is the refactored MyClass

public class MyClass {
    const string REGEX_OPERATORS = "^(?<Id>.{4})(?<Name>.{40})(?<Password>.{5})";
    private readonly Regex reOperators = new Regex(REGEX_OPERATORS, RegexOptions.Compiled);
    private readonly IFileSystem File;
    private readonly IProjectContext context;

    public MyClass(IFileSystem File, IProjectContext context) {
        this.File = File;
        this.context = context;
    }

    public void ImportOperatorList() {
        var path = @"F:\testdata.txt";
        var lines = File.ReadAllLines(path);
        foreach (var line in lines) {
            var match = reOperators.Match(line);
            if (match.Success) {
                string rawId = match.Groups["Id"].Value;
                string rawName = match.Groups["Name"].Value;
                string rawPassword = match.Groups["Password"].Value;
                var op = new Operator {
                    Id = int.Parse(rawId, System.Globalization.NumberStyles.Integer),
                    Name = rawName,
                    Password = int.Parse(rawPassword, System.Globalization.NumberStyles.Integer)
                };
                context.Operators.Add(op);
            }
        }
        if (lines.Length > 0)
            Debug.WriteLine(context.SaveChanges());
    }
}

With the following modifications

public interface IFileSystem {
    string[] ReadAllLines(string path);
}

public class FileWrapper : IFileSystem {
    public string[] ReadAllLines(string path) {
        var lines = File.ReadAllLines(path);
        return lines;
    }
}

public interface IProjectContext : IDisposable {
    DbSet<Operator> Operators { get; set; }
    int SaveChanges();
    //...add other functionality that needs to be exposed as needed
    //eg: Database Database { get; }
    //...
}

public class MyProjectContext : DbContext, IProjectContext {
    public MyProjectContext(DbContextOptions<MyProjectContext> options) : base(options) { }

    public DbSet<Operator> Operators { get; set; }
}

You would make sure all the abstractions are registered with the service container at the composition root.

public void ConfigureServices(IServiceCollection services) {
    services.AddDbContext<MyProjectContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MyProjectContext")));
    services.AddHangfire(options => options.UseSqlServerStorage(Configuration.GetConnectionString("MyProjectContext")));
    services.AddOptions();
    services.Configure<MySettings>(options => Configuration.GetSection("MySettings").Bind(options));
    services.AddMvc().AddDataAnnotationsLocalization();

    //...adding additional services
    services.AddScoped<IProjectContext, MyProjectContext>();
    services.AddTransient<IFileSystem, FileWrapper>();
    services.AddTransient<MyClass, MyClass>();
}

Now when using the scoped service provider you can ask for your class and all the dependencies will be injected when resolving MyClass

using (var scope = host.Services.CreateScope()) {
    var services = scope.ServiceProvider;
    var myClass = services.GetRequiredService<MyClass>();
    myClass.ImportOperatorList();
}

As the above is scoped, the container will manage the disposal of any services created by the container when it goes out of scope.

Solution 2

You have to pass the context argument to the function manually, dependency injection doesn't do this for you. Hence, in program.cs you might add:

using (var scope = host.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<MyProjectContext>();

    // pass context to relevant Classes/Functions, i.e.
    MyClass myClass = new MyClass();
    myClass.ImportOperatorList(context);
}

In MyClass.cs now you can directly use that variable:

public void ImportOperatorList(MyProjectContext context)
{
    // ...
    context.Operators.Add(op);
    context.SaveChanges();
}
Share:
25,382
Mark
Author by

Mark

Interests: C++, Qt5, Raspberry Pi, Linux and Android s.o.

Updated on August 21, 2020

Comments

  • Mark
    Mark over 3 years

    I don't understand the official documentation, at the paragraph about dependency injection.

    They say I can use a controller (but from here I know I don't need it because I'm using Razor pages) or I can access directly to ServiceProvider:

    using (var context = serviceProvider.GetService<BloggingContext>())
    {
      // do stuff
    }
    

    but how to retrieve the reference to the ServiceProvider in a generic C# class of my project?

    I setup the services in startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<MyDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MyDbContext")));
        services.AddHangfire(options => options.UseSqlServerStorage(Configuration.GetConnectionString("MyDbContext")));
        services.AddOptions();
        services.Configure<MySettings>(options => Configuration.GetSection("MySettings").Bind(options));
        services.AddMvc().AddDataAnnotationsLocalization();
    }
    

    EDIT

    To further clarify my confusion, what I'm trying to do is to add/get data from a Worker class. Here I found an example how to do it:

    using (var context = new BloggingContext())
    {
        var blog = new Blog { Url = "http://sample.com" };
        context.Blogs.Add(blog);
        context.SaveChanges();
    
        Console.WriteLine(blog.BlogId + ": " +  blog.Url);
    }
    

    But I cannot use a constructor without the argument DbContext if I'm going to use dependency injection. On the other side, if I add the argument, I have to pass the right value when I call the constructor as in the above example - and this is the initial question.

    EDIT2

    I'm going to post a "complete" example. It's hard to me to understand, but I'm trying anyway:

    program.cs

    using Hangfire;
    using Microsoft.AspNetCore;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Options;
    
    namespace MyProject
    {
        public class Program
        {
    
            public static void Main(string[] args)
            {
                IWebHost host = BuildWebHost(args);
                BackgroundJob.Enqueue<MyClass>(x => x.ImportOperatorList());
                host.Run();
            }
    
            public static IWebHost BuildWebHost(string[] args) =>
                WebHost.CreateDefaultBuilder(args)
                    .UseStartup<Startup>()
                    .Build();
    
        }
    }
    

    startup.cs

    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Hangfire;
    using MyProject.Models;
    
    namespace MyProject
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            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.AddDbContext<MyProjectContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MyProjectContext")));
                services.AddHangfire(options => options.UseSqlServerStorage(Configuration.GetConnectionString("MyProjectContext")));
                services.AddOptions();
                services.Configure<MySettings>(options => Configuration.GetSection("MySettings").Bind(options));
                services.AddMvc().AddDataAnnotationsLocalization();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                    app.UseBrowserLink();
                }
                else
                {
                    app.UseExceptionHandler("/Error");
                }
    
                app.UseStaticFiles();
                app.UseHangfireDashboard();
                app.UseHangfireServer();
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller}/{action=Index}/{id?}");
                });
            }
        }
    }
    

    MyProjectContext.cs

    using Microsoft.EntityFrameworkCore;
    
    namespace MyProject.Models
    {
        public class MyProjectContext : DbContext
        {
            public MyProjectContext(DbContextOptions<MyProjectContext> options) : base(options) { }
    
            public DbSet<Operator> Operators { get; set; }
        }
    
        public class Operator
        {
            public int Id { get; set; }
            [MaxLength(40)]
            public string Name { get; set; }
            public int Password { get; set; }
        }
    }
    

    MyClass.cs

    using MyProject.Models;
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Text.RegularExpressions;
    
    namespace MyProject
    {
        public class MyClass
        {
            const string REGEX_OPERATORS = "^(?<Id>.{4})(?<Name>.{40})(?<Password>.{5})";
            private readonly Regex reOperators = new Regex(REGEX_OPERATORS, RegexOptions.Compiled);
    
            public void ImportOperatorList()
            {
                var path = @"F:\testdata.txt";
                string[] lines = File.ReadAllLines(path);
    
                foreach (var line in lines)
                {
                    Match match = reOperators.Match(line);
                    if (match.Success)
                    {
                        string rawId = match.Groups["Id"].Value;
                        string rawName = match.Groups["Name"].Value;
                        string rawPassword = match.Groups["Password"].Value;
    
                        int Id;
                        try
                        {
                            Id = int.Parse(rawId, System.Globalization.NumberStyles.Integer);
                        }
                        catch (Exception)
                        {
                            throw;
                        }
    
                        string Name = rawName;
    
                        int Password;
                        try
                        {
                            Password = int.Parse(rawPassword, System.Globalization.NumberStyles.Integer);
                        }
                        catch (Exception)
                        {
                            throw;
                        }
    
                        using (var context = new MyProjectContext(/* ??? */))
                        {
                            var op = new Operator
                            {
                                Id = Id,
                                Name = Name,
                                Password = Password
                            };
    
                            context.Operators.Add(op);
                            Debug.WriteLine(context.SaveChanges());
                        }
                    }
                }
            }
        }
    }
    

    Of course isn't complete nor compilable, because there are a lot of other files in the project (even without my own specific application).

    • maccettura
      maccettura over 6 years
      What does your view, controller and action look like?
    • Crowcoder
      Crowcoder over 6 years
      This documentation appears to show you exactly how to inject a db context with razor pages.
    • Mark
      Mark over 6 years
      As said I have no controller. And I'm able to use data from razor pages. My question is specific: I'm talking about a C# class, and how to pass the right parameters to the constructor. Or I have completely mis-understood what you're saying?
    • Nkosi
      Nkosi over 6 years
      @Mark The question in its current state is unclear as it is incomplete. Read How to Ask and then provide a minimal reproducible example that can be used to better understand and reproduce your problem.
    • Nkosi
      Nkosi over 6 years
      @Mark Crowcoder's provided link shows you how to inject db context into razor page
    • Mark
      Mark over 6 years
      It's almost impossible to post a complete example of an ASP.NET project. I posted the code I think it's relevant. I will gladly add a specific function if you will ask for. The question is pretty simple: "how to call the constructor of DbContext from a C# class in order to add data to the database".
    • Mark
      Mark over 6 years
      @Nkosi I saw it, but I'm not asking help about how to inject db context in razor pages (I'm able to to it) instead how to do this from a C# class. Exactly like the last example I added but with the correct argument passed to the constructor of the DbContext.
    • Sefe
      Sefe over 6 years
      You don't have to post a complete example of your project. But you can post an example that reduces your issue to one executable example.
  • Nkosi
    Nkosi over 6 years
    What is stopping you from making the context the dependency via constructor and registering MyClass with the service container so that when resolving the class it is injected into the class?
  • Mark
    Mark over 6 years
    Well, if I knew that I can do this way I wouldn't post the question! I will try this. Feel free to provide an alternative answer.
  • Nkosi
    Nkosi over 6 years
    The issue was that we were not understanding what it is you wanted. The communication was causing confusion on both sides. That is why we were asking for more explanations. I will see if I understand what it is you wanted and post something building upon what you provided here
  • Mark
    Mark over 6 years
    Thanks for the time spent helping me. I think there are few typos on IProjectContext and IMyProjectContext, but I have a question about the Interface approach. I have to expose all functions and properties I need to call from MyClass. Would you mind to explain me how to expose, in example, the Database property? I need it to call ExecuteSqlCommand.
  • Nkosi
    Nkosi over 6 years
    @Mark Add what you need exposed to the interface. The context already exposes these so putting them in the interface will make them available to those dependent on that interface. Check updated answer.
  • Mark
    Mark over 6 years
    I tried to add DatabaseFacade Database { get; } but it tries to override the original member DbContext.Database. What's the right syntax here?
  • Nkosi
    Nkosi over 6 years
    @Mark, Yeah that will brake the current approach. That is one of the drawback of coupling to DbContext. What I usually do is just create my interface create a completely new class based on that interface and wrap the dbcontext. that way I have total control of what the interface exposes and how it is exposed. which is more of a repository pattern
  • Mark
    Mark over 6 years
    Both SaveChanges and Database are members of DbContext. Why it's not allowed to expose the latter?
  • Mark
    Mark over 6 years
    Anyway I easily fixed adding just a wrapper for Database.ExecuteSqlCommand()