Building database applications in Blazor - Part 2 - Services - building CRUD data layer

catalogue Repositories and databases target service gener...
generic paradigm
DbTaskResult
WeatherForecast
WeatherForecastDBContext
LocalWeatherDbContext
InMemoryWeatherDbContext
DbContextExtensions
IFactoryDataService
FactoryDataService
FactoryServerDataService
API controller
FactoryServerInMemoryDataService
Controller service
IFactoryControllerService
FactoryControllerService
WeatherForecastControllerService

catalogue

Repositories and databases

target

service

generic paradigm

data access

DbTaskResult

Data class

WeatherForecast

Entity frame layer

WeatherForecastDBContext

LocalWeatherDbContext

InMemoryWeatherDbContext

DbContextExtensions

IFactoryDataService

FactoryDataService

FactoryServerDataService

API controller

FactoryServerInMemoryDataService

Controller service

IFactoryControllerService

FactoryControllerService

WeatherForecastControllerService

summary

This is the second article in the series on building Blazor database applications. It describes how to build the data and business logic layers into common library code, making it easy to deploy application specific data services. It is a complete rewrite of earlier versions.

This series of articles is as follows:

  1. Project structure and framework.
  2. Services - building the CRUD data layer.
  3. View component - CRUD editing and viewing operations in the UI.
  4. UI components -- building HTML/CSS controls.
  5. View component - CRUD list operation in UI.
Repositories and databases

Repository moved to CEC.Database repository . You can use it as a template for developing your own applications. The previous repository is obsolete and will be deleted.

There is an SQL script in / SQL in the repository to build the database. The application can use a real SQL database or an in memory SQLite database.

You can see the Server and WASM versions of the projects running here on the same site.

target

Before delving into the details, let's take a look at our goal: to build library code and declare a standard UI controller service, which is as simple as this:

public class WeatherForecastControllerService : FactoryControllerService<WeatherForecast> { public WeatherForecastControllerService(IFactoryDataService factoryDataService) : base(factoryDataService) { } }

And declare a database DbContext as follows:

public class LocalWeatherDbContext : DbContext { public LocalWeatherDbContext(DbContextOptions<LocalWeatherDbContext> options) : base(options) {} // A DbSet per database entity public DbSet<WeatherForecast> WeatherForecast { get; set; } }

The process of adding a new database entity is:

  1. Add the necessary tables to the database.
  2. Define a data class.
  3. Define DbSet in DbContext.
  4. Define a public class nnnncontrollerservice service and register it with the service container.

Some entities can be complex, but this does not invalidate the method -- more than 80% of the code in the library.

service

Blazor is based on the principles of DI [dependency injection] and IOC [inversion of control]. If you are not familiar with these concepts, please do some research before delving into blazor. In the long run, it will save you time!

Blazor Singleton and Transient services are relatively simple. You can In Microsoft documents Read more about them. Scoped is a little more complicated.

  1. Scope service objects exist throughout the lifetime of a client application session -- note that the client, not the server. Any application reset, such as F5 or navigation away from the application, resets all scope services. The repeat tab in the browser creates a new application and a new set of scope services.
  2. The scope service can be further limited to a single object in the code. OwningComponentBase component classes have the function to limit the life of a range of services to the life of components.

Services is the Blazor IOC [inversion of control] container. The service instance is declared as follows:

  1. In ConfigureServices on the Blazor server Startup.cs
  2. In the Blazor WASM Program.cs.

The solution uses a service collection extension method, such as add application services, to centralize all application specific services under one roof.

// Blazor.Database.Web/startup.cs public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); // the local application Services defined in ServiceCollectionExtensions.cs // services.AddApplicationServices(this.Configuration); services.AddInMemoryApplicationServices(this.Configuration); }

Extensions are declared as static extension methods in static classes. These two methods are shown below.

//Blazor.Database.Web/Extensions/ServiceCollectionExtensions.cs public static class ServiceCollectionExtensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration) { // Local DB Setup var dbContext = configuration.GetValue<string>("Configuration:DBContext"); services.AddDbContextFactory<LocalWeatherDbContext>(options => options.UseSqlServer(dbContext), ServiceLifetime.Singleton); services.AddSingleton<IFactoryDataService, LocalDatabaseDataService>(); services.AddScoped<WeatherForecastControllerService>(); return services; } public static IServiceCollection AddInMemoryApplicationServices(this IServiceCollection services, IConfiguration configuration) { // In Memory DB Setup var memdbContext = "Data Source=:memory:"; services.AddDbContextFactory<InMemoryWeatherDbContext>(options => options.UseSqlite(memdbContext), ServiceLifetime.Singleton); services.AddSingleton<IFactoryDataService, TestDatabaseDataService>(); services.AddScoped<WeatherForecastControllerService>(); return services; } }

In program.cs of WASM project:

// program.cs public static async Task Main(string[] args) { ..... // Added here as we don't have access to builder in AddApplicationServices builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); // the Services for the Application builder.Services.AddWASMApplicationServices(); ..... }
// ServiceCollectionExtensions.cs public static IServiceCollection AddWASMApplicationServices(this IServiceCollection services) { services.AddScoped<IFactoryDataService, FactoryWASMDataService>(); services.AddScoped<WeatherForecastControllerService>(); return services; }

main points:

  1. Each project / library has an IServiceCollection extension method to encapsulate the specific services required by the project.
  2. Only data tier services are different. The server version used by the Blazor server and WASM API server interfaces with the database and Entity Framework. Its scope is singleton.
  3. Everything is asynchronous, using DbContextFactory and managing DbContext instances when in use. The WASM client version uses HttpClient (which is a scoped service) to call the API, so it is scoped.
  4. The IFactoryDataService that implements IFactoryDataService handles all data requests through generics. TRecord defines which dataset to retrieve and return. All core data service codes of the factory service template.
  5. There are both real SQL databases and SQLite in memory   DbContext.

generic paradigm

Factory library code relies heavily on generics. Two common entities are defined:

  1. TRecord represents the model record class. It must be a class that implements IDbRecord and defines an empty new().   TRecord is used at the method level.
  2. TDbContext is the database context. It must inherit from the DbContext class.

Class declaration is as follows:

//Blazor.SPA/Services/FactoryDataService.cs public abstract class FactoryDataService<TContext>: IFactoryDataService<TContext> where TContext : DbContext ...... // example method template public virtual Task<TRecord> GetRecordAsync<TRecord>(int id) where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(new TRecord());

data access

Before delving into the details, let's take a look at the main CRUDL methods we need to implement:

  1. GetRecordList -- get the list of records in the dataset. This can be paged and sorted.
  2. GetRecord -- get a single record by ID
  3. CreateRecord -- create a new record
  4. UpdateRecord -- updates a record based on its ID
  5. DeleteRecord -- deletes a record according to its ID

Keep these in mind as we read this article.

DbTaskResult

The data layer CUD operation returns a DbTaskResult object. Most properties are self-evident. It is intended to be used by the UI to build CSS framework entities, such as alerts and Toast. NewID returns a new ID from the create operation  .

public class DbTaskResult { public string Message { get; set; } = "New Object Message"; public MessageType Type { get; set; } = MessageType.None; public bool IsOK { get; set; } = true; public int NewID { get; set; } = 0; }

Data class

The data class implements IDbRecord.

  1. ID is the standard database identification field. Usually an int.
  2. GUID   Is the unique identifier of the copy of this record.
  3. DisplayName provides a common name for the record. We can use it in titles and other UI components.

public interface IDbRecord<TRecord> where TRecord : class, IDbRecord<TRecord>, new() { public int ID { get; } public Guid GUID { get; } public string DisplayName { get; } }

WeatherForecast

This is the data class of the WeatherForecast data entity.

main points:

  1. The entity frame attribute used for the attribute tag.
  2. Implementation of IDbRecord.
  3. Implementation of IValidation. We will introduce custom validation in the third article.

public class WeatherForecast : IValidation, IDbRecord<WeatherForecast> { [Key] public int ID { get; set; } = -1; public DateTime Date { get; set; } = DateTime.Now; public int TemperatureC { get; set; } = 0; [NotMapped] public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } = string.Empty; [NotMapped] public Guid GUID { get; init; } = Guid.NewGuid(); [NotMapped] public string DisplayName => $"Weather Forecast for "; public bool Validate(ValidationMessageStore validationMessageStore, string fieldname, object model = null) { model = model ?? this; bool trip = false; this.Summary.Validation("Summary", model, validationMessageStore) .LongerThan(2, "Your description needs to be a little longer! 3 letters minimum") .Validate(ref trip, fieldname); this.Date.Validation("Date", model, validationMessageStore) .NotDefault("You must select a date") .LessThan(DateTime.Now.AddMonths(1), true, "Date can only be up to 1 month ahead") .Validate(ref trip, fieldname); this.TemperatureC.Validation("TemperatureC", model, validationMessageStore) .LessThan(70, "The temperature must be less than 70C") .GreaterThan(-60, "The temperature must be greater than -60C") .Validate(ref trip, fieldname); return !trip; }

Entity frame layer

The application implements two Entity Framework DBContext classes.

WeatherForecastDBContext

The DbContext has each record type of DbSet. Each DbSet is linked to a view in OnModelCreating(). The WeatherForecast application has a record type.

LocalWeatherDbContext

This class is very basic and creates a dbset for each data class. Dbset must have the same name as the data class.

public class LocalWeatherDbContext : DbContext { private readonly Guid _id; public LocalWeatherDbContext(DbContextOptions<LocalWeatherDbContext> options) : base(options) => _id = Guid.NewGuid(); public DbSet<WeatherForecast> WeatherForecast { get; set; } }

InMemoryWeatherDbContext

The in memory version is slightly more complex and requires the database to be built and populated on the fly.

public class InMemoryWeatherDbContext : DbContext { private readonly Guid _id; public InMemoryWeatherDbContext(DbContextOptions<InMemoryWeatherDbContext> options) : base(options) { this._id = Guid.NewGuid(); this.BuildInMemoryDatabase(); } public DbSet<WeatherForecast> WeatherForecast { get; set; } private void BuildInMemoryDatabase() { var conn = this.Database.GetDbConnection(); conn.Open(); var cmd = conn.CreateCommand(); cmd.CommandText = "CREATE TABLE [WeatherForecast]([ID] INTEGER PRIMARY KEY AUTOINCREMENT, [Date] [smalldatetime] NOT NULL, [TemperatureC] [int] NOT NULL, [Summary] [varchar](255) NULL)"; cmd.ExecuteNonQuery(); foreach (var forecast in this.NewForecasts) { cmd.CommandText = $"INSERT INTO WeatherForecast([Date], [TemperatureC], [Summary]) VALUES('', , '')"; cmd.ExecuteNonQuery(); } } private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private List<WeatherForecast> NewForecasts { get { { var rng = new Random(); return Enumerable.Range(1, 10).Select(index => new WeatherForecast { //ID = index, Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }).ToList(); } } }

DbContextExtensions

We use generics, so we need a way to get the DbSet of the data class declared as TRecord. This is the DbContext implemented as an extension method. For this purpose, each DbSet name should have the same name as the data class. If the names are different, dbSetName provides a backup.  

This method uses reflection to   TRecord finds the DbSet.

public static DbSet<TRecord> GetDbSet<TRecord>(this DbContext context, string dbSetName = null) where TRecord : class, IDbRecord<TRecord>, new() { var recname = new TRecord().GetType().Name; // Get the property info object for the DbSet var pinfo = context.GetType().GetProperty(dbSetName ?? recname); DbSet<TRecord> dbSet = null; // Get the property DbSet try { dbSet = (DbSet<TRecord>)pinfo.GetValue(context); } catch { throw new InvalidOperationException($" does not have a matching DBset "); } Debug.Assert(dbSet != null); return dbSet; }

IFactoryDataService

IFactoryDataService defines the basic CRUDL methods that DataServices must implement. Data services are defined in the service container and used through interfaces. Note the TRecord for each method and its constraints. There are two GetRecordListAsync methods. One gets the entire dataset, and the other uses the PaginstorData object to page and sort the dataset. More about Paginator is in the fifth article.

public interface IFactoryDataService { public Task<List<TRecord>> GetRecordListAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new(); public Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData) where TRecord : class, IDbRecord<TRecord>, new(); public Task<TRecord> GetRecordAsync<TRecord>(int id) where TRecord : class, IDbRecord<TRecord>, new(); public Task<int> GetRecordListCountAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new(); public Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new(); public Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new(); public Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new(); }

FactoryDataService

FactoryDataService is an abstract implementation of IFactoryDataService. It provides default records, lists, or unimplemented   DBTaskResult message.  

public abstract class FactoryDataService: IFactoryDataService { public Guid ServiceID { get; } = Guid.NewGuid(); public IConfiguration AppConfiguration { get; set; } public FactoryDataService(IConfiguration configuration) => this.AppConfiguration = configuration; public virtual Task<List<TRecord>> GetRecordListAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(new List<TRecord>()); public virtual Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData) where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(new List<TRecord>()); public virtual Task<TRecord> GetRecordAsync<TRecord>(int id) where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(new TRecord()); public virtual Task<int> GetRecordListCountAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(0); public virtual Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(new DbTaskResult() { IsOK = false, Type = MessageType.NotImplemented, Message = "Method not implemented" }); public virtual Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(new DbTaskResult() { IsOK = false, Type = MessageType.NotImplemented, Message = "Method not implemented" }); public virtual Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new() => Task.FromResult(new DbTaskResult() { IsOK = false, Type = MessageType.NotImplemented, Message = "Method not implemented" }); }

FactoryServerDataService

This is a specific server-side implementation. Each database operation is implemented using a separate DbContext instance. GetDBSet is used to get the notes of the correct DBSet for TRecord.

public class FactoryServerDataService<TDbContext> : FactoryDataService where TDbContext : DbContext { protected virtual IDbContextFactory<TDbContext> DBContext { get; set; } = null; public FactoryServerDataService(IConfiguration configuration, IDbContextFactory<TDbContext> dbContext) : base(configuration) => this.DBContext = dbContext; public override async Task<List<TRecord>> GetRecordListAsync<TRecord>() => await this.DBContext .CreateDbContext() .GetDbSet<TRecord>() .ToListAsync() ?? new List<TRecord>(); public override async Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData) { var startpage = paginatorData.Page <= 1 ? 0 : (paginatorData.Page - 1) * paginatorData.PageSize; var context = this.DBContext.CreateDbContext(); var dbset = this.DBContext .CreateDbContext() .GetDbSet<TRecord>(); var x = typeof(TRecord).GetProperty(paginatorData.SortColumn); var isSortable = typeof(TRecord).GetProperty(paginatorData.SortColumn) != null; if (isSortable) { var list = await dbset .OrderBy(paginatorData.SortDescending ? $" descending" : paginatorData.SortColumn) .Skip(startpage) .Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>(); return list; } else { var list = await dbset .Skip(startpage) .Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>(); return list; } } public override async Task<TRecord> GetRecordAsync<TRecord>(int id) => await this.DBContext. CreateDbContext(). GetDbSet<TRecord>(). FirstOrDefaultAsync(item => ((IDbRecord<TRecord>)item).ID == id) ?? default; public override async Task<int> GetRecordListCountAsync<TRecord>() => await this.DBContext.CreateDbContext().GetDbSet<TRecord>().CountAsync(); public override async Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record) { var context = this.DBContext.CreateDbContext(); context.Entry(record).State = EntityState.Modified; return await this.UpdateContext(context); } public override async Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record) { var context = this.DBContext.CreateDbContext(); context.GetDbSet<TRecord>().Add(record); return await this.UpdateContext(context); } public override async Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record) { var context = this.DBContext.CreateDbContext(); context.Entry(record).State = EntityState.Deleted; return await this.UpdateContext(context); } protected async Task<DbTaskResult> UpdateContext(DbContext context) => await context.SaveChangesAsync() > 0 ? DbTaskResult.OK() : DbTaskResult.NotOK(); }

The FactoryWASMDataService looks a little different. It implements the interface, but is used for HttpClient to get / publish the API to the server.

The service mapping is as follows:

UI controller service = > wasmdataservice = > API controller = > serverdataservice = > dbcontext

public class FactoryWASMDataService : FactoryDataService, IFactoryDataService { protected HttpClient HttpClient { get; set; } public FactoryWASMDataService(IConfiguration configuration, HttpClient httpClient) : base(configuration) => this.HttpClient = httpClient; public override async Task<List<TRecord>> GetRecordListAsync<TRecord>() => await this.HttpClient.GetFromJsonAsync<List<TRecord>>($"/list"); public override async Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData) { var response = await this.HttpClient.PostAsJsonAsync($"/listpaged", paginatorData); return await response.Content.ReadFromJsonAsync<List<TRecord>>(); } public override async Task<TRecord> GetRecordAsync<TRecord>(int id) { var response = await this.HttpClient.PostAsJsonAsync($"/read", id); var result = await response.Content.ReadFromJsonAsync<TRecord>(); return result; } public override async Task<int> GetRecordListCountAsync<TRecord>() => await this.HttpClient.GetFromJsonAsync<int>($"/count"); public override async Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record) { var response = await this.HttpClient.PostAsJsonAsync<TRecord>($"/update", record); var result = await response.Content.ReadFromJsonAsync<DbTaskResult>(); return result; } public override async Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record) { var response = await this.HttpClient.PostAsJsonAsync<TRecord>($"/create", record); var result = await response.Content.ReadFromJsonAsync<DbTaskResult>(); return result; } public override async Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record) { var response = await this.HttpClient.PostAsJsonAsync<TRecord>($"/update", record); var result = await response.Content.ReadFromJsonAsync<DbTaskResult>(); return result; } protected string GetRecordName<TRecord>() where TRecord : class, IDbRecord<TRecord>, new() => new TRecord().GetType().Name; }

API controller

The controller is implemented in the Web project, one for each DataClass.

The WeatherForecast controller is shown below. It basically passes the request to the FactoryServerDataService through the IFactoryService interface.

[ApiController] public class WeatherForecastController : ControllerBase { protected IFactoryDataService DataService { get; set; } private readonly ILogger<WeatherForecastController> logger; public WeatherForecastController(ILogger<WeatherForecastController> logger, IFactoryDataService dataService) { this.DataService = dataService; this.logger = logger; } [MVC.Route("weatherforecast/list")] [HttpGet] public async Task<List<WeatherForecast>> GetList() => await DataService.GetRecordListAsync<WeatherForecast>(); [MVC.Route("weatherforecast/listpaged")] [HttpGet] public async Task<List<WeatherForecast>> Read([FromBody] PaginatorData data) => await DataService.GetRecordListAsync<WeatherForecast>( paginator: data); [MVC.Route("weatherforecast/count")] [HttpGet] public async Task<int> Count() => await DataService.GetRecordListCountAsync<WeatherForecast>(); [MVC.Route("weatherforecast/get")] [HttpGet] public async Task<WeatherForecast> GetRec(int id) => await DataService.GetRecordAsync<WeatherForecast>(id); [MVC.Route("weatherforecast/read")] [HttpPost] public async Task<WeatherForecast> Read([FromBody]int id) => await DataService.GetRecordAsync<WeatherForecast>(id); [MVC.Route("weatherforecast/update")] [HttpPost] public async Task<DbTaskResult> Update([FromBody]WeatherForecast record) => await DataService.UpdateRecordAsync<WeatherForecast>(record); [MVC.Route("weatherforecast/create")] [HttpPost] public async Task<DbTaskResult> Create([FromBody]WeatherForecast record) => await DataService.CreateRecordAsync<WeatherForecast>(record); [MVC.Route("weatherforecast/delete")] [HttpPost] public async Task<DbTaskResult> Delete([FromBody] WeatherForecast record) => await DataService.DeleteRecordAsync<WeatherForecast>(record); }

FactoryServerInMemoryDataService

For testing and demonstration, there is another server data service using SQLite in memory dbcontext.

This code is similar to FactoryServerDataService, but uses a single DbContext for all transactions.

public class FactoryServerInMemoryDataService<TDbContext> : FactoryDataService, IFactoryDataService where TDbContext : DbContext { protected virtual IDbContextFactory<TDbContext> DBContext { get; set; } = null; private DbContext _dbContext; public FactoryServerInMemoryDataService(IConfiguration configuration, IDbContextFactory<TDbContext> dbContext) : base(configuration) { this.DBContext = dbContext; _dbContext = this.DBContext.CreateDbContext(); } public override async Task<List<TRecord>> GetRecordListAsync<TRecord>() { var dbset = _dbContext.GetDbSet<TRecord>(); return await dbset.ToListAsync() ?? new List<TRecord>(); } public override async Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData) { var startpage = paginatorData.Page <= 1 ? 0 : (paginatorData.Page - 1) * paginatorData.PageSize; var dbset = _dbContext.GetDbSet<TRecord>(); var isSortable = typeof(TRecord).GetProperty(paginatorData.SortColumn) != null; if (isSortable) { var list = await dbset .OrderBy(paginatorData.SortDescending ? $" descending" : paginatorData.SortColumn) .Skip(startpage) .Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>(); return list; } else { var list = await dbset .Skip(startpage) .Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>(); return list; } } public override async Task<TRecord> GetRecordAsync<TRecord>(int id) { var dbset = _dbContext.GetDbSet<TRecord>(); return await dbset.FirstOrDefaultAsync(item => ((IDbRecord<TRecord>)item).ID == id) ?? default; } public override async Task<int> GetRecordListCountAsync<TRecord>() { var dbset = _dbContext.GetDbSet<TRecord>(); return await dbset.CountAsync(); } public override async Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record) { _dbContext.Entry(record).State = EntityState.Modified; var x = await _dbContext.SaveChangesAsync(); return new DbTaskResult() { IsOK = true, Type = MessageType.Success }; } public override async Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record) { var dbset = _dbContext.GetDbSet<TRecord>(); dbset.Add(record); var x = await _dbContext.SaveChangesAsync(); return new DbTaskResult() { IsOK = true, Type = MessageType.Success, NewID = record.ID }; } public override async Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record) { _dbContext.Entry(record).State = EntityState.Deleted; var x = await _dbContext.SaveChangesAsync(); return new DbTaskResult() { IsOK = true, Type = MessageType.Success }; } }

Controller service

The controller service is the interface between the data service and the UI. They implement the logic needed to manage the data classes for which they are responsible. Although most of the code resides in FactoryControllerService, it is inevitable that there will be some data class specific code.

IFactoryControllerService

IFactoryControllerService   Defines the public interface used by the basic form code.

be careful:

  1. Generic TRecord
  2. Save the properties of the current record and the record list.
  3. Boolean logical attributes used to simplify state management.
  4. Events that record and list changes.
  5. Reset method to reset services / records / lists.
  6. Update / use the CRUDL method of the current record / list.

public interface IFactoryControllerService<TRecord> where TRecord : class, IDbRecord<TRecord>, new() { public Guid Id { get; } public TRecord Record { get; } public List<TRecord> Records { get; } public int RecordCount => this.Records?.Count ?? 0; public int RecordId { get; } public Guid RecordGUID { get; } public DbTaskResult DbResult { get; } public Paginator Paginator { get; } public bool IsRecord => this.Record != null && this.RecordId > -1; public bool HasRecords => this.Records != null && this.Records.Count > 0; public bool IsNewRecord => this.IsRecord && this.RecordId == -1; public event EventHandler RecordHasChanged; public event EventHandler ListHasChanged; public Task Reset(); public Task ResetRecordAsync(); public Task ResetListAsync(); public Task GetRecordsAsync() => Task.CompletedTask; public Task<bool> SaveRecordAsync(); public Task<bool> GetRecordAsync(int id); public Task<bool> NewRecordAsync(); public Task<bool> DeleteRecordAsync(); }

FactoryControllerService

FactoryControllerService is an abstract implementation of IFactoryControllerService. It contains all the template code. Most of the code is self-evident.  

public abstract class FactoryControllerService<TRecord> : IDisposable, IFactoryControllerService<TRecord> where TRecord : class, IDbRecord<TRecord>, new() { // unique ID for this instance public Guid Id { get; } = Guid.NewGuid(); // Record Property. Triggers Event when changed. public TRecord Record { get => _record; private set { this._record = value; this.RecordHasChanged?.Invoke(value, EventArgs.Empty); } } private TRecord _record = null; // Recordset Property. Triggers Event when changed. public List<TRecord> Records { get => _records; private set { this._records = value; this.ListHasChanged?.Invoke(value, EventArgs.Empty); } } private List<TRecord> _records = null; public int RecordId => this.Record?.ID ?? 0; public Guid RecordGUID => this.Record?.GUID ?? Guid.Empty; public DbTaskResult DbResult { get; set; } = new DbTaskResult(); /// Property for the Paging object that controls paging and interfaces with the UI Paging Control public Paginator Paginator { get; private set; } public bool IsRecord => this.Record != null && this.RecordId > -1; public bool HasRecords => this.Records != null && this.Records.Count > 0; public bool IsNewRecord => this.IsRecord && this.RecordId == -1; /// Data Service for data access protected IFactoryDataService DataService { get; set; } public event EventHandler RecordHasChanged; public event EventHandler ListHasChanged; public FactoryControllerService(IFactoryDataService factoryDataService) { this.DataService = factoryDataService; this.Paginator = new Paginator(10, 5); this.Paginator.PageChanged += this.OnPageChanged; } /// Method to reset the service public Task Reset() { this.Record = null; this.Records = null; return Task.CompletedTask; } /// Method to reset the record list public Task ResetListAsync() { this.Records = null; return Task.CompletedTask; } /// Method to reset the Record public Task ResetRecordAsync() { this.Record = null; return Task.CompletedTask; } /// Method to get a recordset public async Task GetRecordsAsync() { this.Records = await DataService.GetRecordListAsync<TRecord>(this.Paginator.GetData); this.Paginator.RecordCount = await GetRecordListCountAsync(); this.ListHasChanged?.Invoke(null, EventArgs.Empty); } /// Method to get a record /// if id < 1 will create a new record public async Task<bool> GetRecordAsync(int id) { if (id > 0) this.Record = await DataService.GetRecordAsync<TRecord>(id); else this.Record = new TRecord(); return this.IsRecord; } /// Method to get the current record count public async Task<int> GetRecordListCountAsync() => await DataService.GetRecordListCountAsync<TRecord>(); public async Task<bool> SaveRecordAsync() { if (this.RecordId == -1) this.DbResult = await DataService.CreateRecordAsync<TRecord>(this.Record); else this.DbResult = await DataService.UpdateRecordAsync(this.Record); await this.GetRecordsAsync(); return this.DbResult.IsOK; } public async Task<bool> DeleteRecordAsync() { this.DbResult = await DataService.DeleteRecordAsync<TRecord>(this.Record); return this.DbResult.IsOK; } public Task<bool> NewRecordAsync() { this.Record = default(TRecord); return Task.FromResult(false); } protected async void OnPageChanged(object sender, EventArgs e) => await this.GetRecordsAsync(); protected void NotifyRecordChanged(object sender, EventArgs e) => this.RecordHasChanged?.Invoke(sender, e); protected void NotifyListChanged(object sender, EventArgs e) => this.ListHasChanged?.Invoke(sender, e); public virtual void Dispose() {} }

WeatherForecastControllerService

The return of the templating comes from the following weatherforcastcontrollerservice statement:

public class WeatherForecastControllerService : FactoryControllerService<WeatherForecast> { public WeatherForecastControllerService(IFactoryDataService factoryDataService) : base(factoryDataService) { } }

summary

This article shows how to build a data service using a set of abstract classes that implement CRUDL operation template code. I deliberately keep error checking in the code to a minimum to make it more readable. You can implement as little or as much as you need.

Some key points to note:

  1. Use Aysnc code whenever possible. Data access functions are asynchronous.
  2. Generics make most templates possible. They create complexity, but they are worth the effort.
  3. Interfaces are critical for dependency injection and UI templating.

If you read this article in the future, check the readme file in the repository for the latest version of the article set.

https://www.codeproject.com/Articles/5279596/Building-a-Database-Application-in-Blazor-Part-2-S

12 September 2021, 22:07 | Views: 8309

Add new comment

For adding a comment, please log in
or create account

0 comments