Creating mechanism to save dynamic settings locally with ASP.NET Core, Entity Framework Core and SQLite
Next step in TeamScreen development is settings screen. In time I plan to abandon solution with providing credentials to 3rd party services using configuration files and moved them to more user-friendly UI. In today’s post, I’ll start from saving only one setting – an interval between plugin change. Saving other settings will come after the creation of plugin architecture.
The first thing we need to do is to remove existing reference to Microsoft.EntityFrameworkCore.SqlServer – it’s added by default when creating new ASPNET.Core project. After that, I add a reference to Microsoft.EntityFrameworkCore.Sqlite. Next, I’ll add entity – it’s very simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class PluginSetting { public int Id { get; set; } public string Plugin { get; set; } public string Value { get; set; } public static PluginSetting Create(string plugin, string value) { return new PluginSetting { Plugin = plugin, Value = value }; } } |
Different plugins will have a different set of settings. To handle it I plan to serialize those settings to JSON and then save them in Value property. Next, we need to have DbContext containing PluginSetting’s entity set:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class AppDbContext : DbContext { public DbSet<PluginSetting> PluginSettings { get; set; } public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<PluginSetting>().HasKey(x => x.Id); base.OnModelCreating(builder); } } |
DBContextOptions are passed to DbContext using dependency injection, so of course, we need to register that, which is done in Startup class:
1 2 3 4 |
var connString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<AppDbContext>(options => options.UseSqlite(connString) ); |
Having entity and DbContext set up, we need now to create initial DB migration describing how DB is created. I still have identity context which I hope I’ll use in future for managing users. With newly added DbContext, we have two, so when creating migrations, we need to set which context we set:
1 2 |
dotnet ef migrations add InitialCreate --context AppDbcontext dotnet ef database update --context AppDbContext |
Ok, DB should be created when we will access it for the first time. Now, how exactly will look settings, that’ll be saved there? All of them implement interface ISettings:
1 2 3 4 |
public interface ISettings<T> where T : ISettings<T>, new() { T WithDefaultValues(); } |
ISettings contains only one method used to set up initial settings values. In future, all plugins will have settings created for them. For now, I have only CoreSettings for general TeamScreen settings containing only Interval property:
1 2 3 4 5 6 7 8 9 10 |
public class CoreSettings : ISettings<CoreSettings> { public int Interval { get; set; } public CoreSettings WithDefaultValues() { Interval = 60; return this; } } |
Now – SettingsService which role is to save and load settings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public interface ISettingsService { Task<T> Get<T>(string plugin) where T : ISettings<T>, new(); Task Set<T>(string plugin, T value) where T : ISettings<T>, new(); } public class SettingsService : ISettingsService { private readonly AppDbContext _db; public SettingsService(AppDbContext db) { _db = db; } public async Task<T> Get<T>(string plugin) where T : ISettings<T>, new() { var setting = await _db.PluginSettings.FirstOrDefaultAsync(x => x.Plugin == plugin); if (setting == null) return new T().WithDefaultValues(); var deserialized = JsonConvert.DeserializeObject(setting.Value, typeof(T)); return (T)deserialized; } public async Task Set<T>(string plugin, T value) where T : ISettings<T>, new() { var jsonValue = JsonConvert.SerializeObject(value); var existing = await _db.PluginSettings.FirstOrDefaultAsync(x => x.Plugin == plugin); if (existing != null) existing.Value = jsonValue; else await _db.PluginSettings.AddAsync(PluginSetting.Create(plugin, jsonValue)); await _db.SaveChangesAsync(); } } |
We have two methods – Get and Set. Get returns first settings from DB for given plugin, when nothing is found we return default settings. When something is returned from DB I deserialize it to correct settings type. Set serializes settings and saves them in DB, depending if it existed before uses insert or update. Both these methods are generic so I can save/load type-safe settings. To do that I needed to provide additional restrictions on generic type:
1 |
where T : ISettings<T>, new() |
Ok, with that we have a mechanism to dynamically save and load settings. Next thing we need is actual UI for this. For now, it’ll be simple, as we need to only display one setting. We have a main page with a dynamically loaded partial view for “Core” plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<h1>Settings</h1> <ul class="nav nav-tabs"> <li class="active"><a data-toggle="tab" href="#core">Core</a></li> <li><a data-toggle="tab" href="#menu1">TeamCity</a></li> <li><a data-toggle="tab" href="#menu2">JIRA</a></li> </ul> <div class="tab-content"> <div id="core" class="tab-pane fade in active"> </div> <div id="menu1" class="tab-pane fade"> <h3>Menu 1</h3> <p>Some content in menu 1.</p> </div> <div id="menu2" class="tab-pane fade"> <h3>Menu 2</h3> <p>Some content in menu 2.</p> </div> </div> @section Scripts{ <script type="text/javascript"> $("#core").load('@Url.Action("CoreSettings")'); </script> } |
1 2 3 4 5 6 7 8 9 |
@model TeamScreen.Models.Settings.CoreSettings <form asp-controller="Settings" asp-action="Save" method="post"> <div class="form-group"> <label for="interval">Plugin change interval in seconds</label> <input type="number" class="form-control" id="interval" asp-for="Interval" min="5"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> |
We also need a controller for that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class SettingsController : Controller { private readonly ISettingsService _settingsService; public SettingsController(ISettingsService settingsService) { _settingsService = settingsService; } public IActionResult Index() { return View(); } public async Task<PartialViewResult> CoreSettings() { var coreSettings = await _settingsService.Get<CoreSettings>(Const.CorePluginName); return PartialView(coreSettings); } [HttpPost] public void Save(CoreSettings settings) { _settingsService.Set(Const.CorePluginName, settings); } } |
All of that creates this page:
It’s – once again – very simple, but also generic. In future it’ll easy to use it for all sort of settings for another plugins. I hope you liked the solution. Thanks for reading and see you next time 🙂
One thought on “Creating mechanism to save dynamic settings locally with ASP.NET Core, Entity Framework Core and SQLite”