Quantcast
Channel: Developer Express Inc.
Viewing all articles
Browse latest Browse all 2370

Blazor – Twitch Episode 2 Recap – Part 2

$
0
0

If we summarize what we’ve done in Part 1 to enable CRUD operations in the DxGrid, it is not complex at all though it is a descent amount of code. Especially, if you need to do this for 200 entities in your real-world database. It would mean a considerable amount of redundant code.

Also, error handling has been implemented, but not on a level that we want. There is sensitive information being projected in the UI.

Refactoring for better architecture (and less code)

In the second part of the video, I have done some refactoring to deal with the issues mentioned above.

DTO Mapping

One of the things I would NOT recommend is to use EF Models directly in the UI. This gives us an opportunity to fine tune the UI Model for its task … the UI.

When using DTO, we’ll need to be able to copy property values from one type of object to another based on certain conventions. An example is shown in the Grid_EditModelSaving event. In this case the convention is name based.

// get item from db and replace properties edited
var db = await ctx.FindAsync<Customer>(item.CustomerId);
if (db != null)
{
    db.FirstName = item.FirstName;
    db.LastName = item.LastName;
    db.Company = item.Company;
    db.Address = item.Address;
    db.City = item.City;
    db.State = item.State;
    db.Country = item.Country;
    db.PostalCode = item.PostalCode;
    db.Phone = item.Phone;
    db.Fax = item.Fax;
    db.Email = item.Email;     
}

In real-world scenario’s there might be other conventions to determine which property will be copied.

Fortunately, there are several NuGet packages available which will do exactly this without the need to write the objA.PropertyValue = objB.PropertyValue statements.

One of the most popular ones is AutoMapper from Jimmy Bogard. It is open-source, free to use and well documented. It uses a configuration profile which describes the conventions being used when DTO is applied. This makes it very flexible.

I will use this one to set up a generic DataStore which will allow us to Query and perform CRUD operations base on DTO Models which will be mapped into EF Models.

This gives us a nice separation of the UI and the actual DataAccess mechanism.

To initialize the mapping configuration we need to create a class which descends from AutoMapper’s Profile class. We can put this class in the RegisterServices.cs file, (or alternatively in a new file), in the DxChinook.Data.EF project.

public class ChinookMappingProfile : Profile
{
    public ChinookMappingProfile() { 
        CreateMap<Customer, CustomerModel>()
            .ReverseMap(); // don't forget the ReverseMap to map both ways
        // ... more mapping conventions here ...
    }
}

This allows us to map from the EF model Customer to CustomerModel and back.

Before this works, we need to initialize the DI container. An appropriate place would be in the RegisterDataServices class in DxChinook.Data.EF project.

public static IServiceCollection RegisterDataServices(
        this IServiceCollection services, string connectionString)
{
    //configure AutoMapper and EF Context
    services.AddAutoMapper(cfg => 
        cfg.AddProfile<ChinookMappingProfile>());

    services.AddDbContext<ChinookContext>(options => 
            options.UseSqlServer(connectionString), 
            ServiceLifetime.Transient);

    return services;
}

Validation

Another thing that I would recommend is to NOT leave validation up to the database, but to validate your input in earlier stages in the process. To improve the end-user experience, we can use DTO Model validation. This means that we don’t need to send the model to the server all the time. Simple things like: Required, Email address, Dates, Min/Max values etc. can be easily validated by the UI - without hitting the DB.

As I mentioned, the DTO Model validation is primarily to improve the end-user experience. Because this sort of validation is happening in the browser, an avid end-user could try to tamper with it, so it is not 100% guaranteed that the data is accurate.

For this – as with any web-technology- it is always necessary to perform server-side validation as well.

.NET comes out of the box with Model validation through attributes found in the System.ComponentModel.DataAnnotations namespace.

This approach works quite nice through all the layers of an application (incl. EF and Blazor) but when you require some more advanced validation – like properties depending on each other properties or objects - it can get tricky. You will need to create your own Validation attributes according to the specifications of your project.

One other popular open-source NuGet package is FluentValidation by Jeremy Skinner. It is – like AutoMapper - open-source and free to use and includes really good documentation for creating our own validation rules.

I will use this package as well and combine it together with AutoMapper in our Store.

The Generic DataStore

Let’s start with creating our generic data store. The main goals for this class are:

  • I want to deal with DTO Models
  • There should be DTO Model Validation
  • I do not care about Entity Framework (or some other data access technology) -> Loosely coupled.
  • There needs to be server-side validation as well.
  • I want to reduce the amount of code in my Blazor components.

Because of the loosely coupled architecture I want to build, let’s add a new Class library project to the solution: DxChinook.Data.csproj

This project will contain the DTO Model classes together with FluentValidators for the DTO Models and it will contain a C# file Interfaces.cs.

The Interfaces.cs file will contain one enum:

public enum DataMode
{
    Create,
    Update,
    Delete
}

We need this later on to determine if we’re Creating, Updating or Deleting, and - as the filename implies - it contains 2 interfaces:

public interface IDataResult
{
    bool Success { get; set; }
    DataMode Mode { get; set; }
    // include namespace FluentValidation for this!
    ValidationException Exception { get; set; }
}

public interface IDataStore<TKey, TModel>
    where TKey : IEquatable<TKey>
    where TModel : class, new()
{
    string[] KeyFields { get; }
    TModel GetByKey(TKey key);
    IQueryable<T> Query<T>() where T : class, new();
    IQueryable<TModel> Query();
    TKey ModelKey(TModel model);
    void SetModelKey(TModel model, TKey key);
    Task<IDataResult> CreateAsync(params TModel[] items);
    Task<IDataResult> UpdateAsync(params TModel[] items);
    Task<IDataResult> DeleteAsync(params TKey[] ids);
}

There are some interesting things to point out in the IDataStore interface. It does not have any knowledge of EF Models. Just DTO Models.

If you cross-check the properties and methods in this interface with the code we’ve build in the Customers.razor code, you might get an impression where this leads to.

Let’s add a new class file in the root of our DxChinook.Data.EF project and call it EFStore.cs.

If will have a class EFResult which will implement the IDataResult interface. This allows us to project success or failure back to the UI.

public class EFResult : IDataResult
{
    public EFResult() { }
    public EFResult(DataMode mode, string propertyName, Exception err)
    {
        Mode = mode;
        Success = (err == null);
        if (!Success)
        {
            Exception = (err as ValidationException)!;
            if (Exception == null)
                Exception = new ValidationException(new[] {
                    new ValidationFailure(propertyName, err!.InnerException == null ? err.Message : err.InnerException.Message)
                });
        }
    }
    public bool Success { get; set; }
    public DataMode Mode { get; set; }
    public ValidationException Exception { get; set; } = default!;        
}

The other class in this file is the EFDataStore. Notice that this is an abstract class.

Its declaration looks like this:

public abstract class EFDataStore<TEFContext, TKey, TModel, TDBModel> 
        : IDataStore<TKey, TModel>
    where TEFContext : DbContext, new()
    where TKey : IEquatable<TKey>
    where TModel : class, new()
    where TDBModel : class, new()
{
    public EFDataStore(TEFContext context, 
        IMapper mapper, 
        IValidator<TDBModel> validator)
    {
        Mapper = mapper;
        DbContext = context;
        Validator = validator;
    }

    protected IMapper Mapper { get; }
    public TEFContext DbContext { get; }
    public IValidator<TDBModel> Validator { get; }
    
    // ... more to come ...
}

A couple of interesting things are happening here. The first generic type is the EF Context class (so you can use this in other projects as well), the second one is the type of the key field. In most cases this will be an int or guid. (In our case an int)

The last 2 generic type are the DTO Model type (TModel) and the EF Entity class (TDBModel). Note that all types have restrictions on them. (where clauses)

As you can also see, the constructor needs 3 parameters - which at runtime - will be injected by the DI container. The IMapper is the AutoMapper instance for mapping fro DTO -> EF and back. The IValidator is a validator class which works with EF type. This covers our server-side validation.

Next we have a couple of abstract methods which need to be implemented for every store.

    // ... class EFDataStore ...
    public abstract string KeyField { get; }
    public abstract void SetModelKey(TModel model, TKey key);
    public abstract TKey ModelKey(TModel model);

    protected abstract TKey DBModelKey(TDBModel model);

You’ll see later when we implement a CustomerStore how this works!

Next there are some methods which deal with querying the datastore based on DTO Models.

    protected virtual TDBModel? EFGetByKey(TKey key) { 
        return DbContext.Find<TDBModel>(key);
    }

    protected virtual IQueryable<TDBModel> EFQuery() {
        return DbContext.Set<TDBModel>();
    }
    
    public TModel CreateModel() => new TModel();
    
    public virtual IQueryable<TModel> Query() {
        return EFQuery().ProjectTo<TModel>(Mapper.ConfigurationProvider);
    }
    
    public virtual IQueryable<T> Query<T>() where T : class, new() {
        return EFQuery().ProjectTo<T>(Mapper.ConfigurationProvider);
    }

    public virtual TModel GetByKey(TKey key)
    {
        TModel result = CreateModel();
        return Mapper.Map(EFGetByKey(key), result);
    }

If you look closely to these methods, you’ll see AutoMapper in action and it does an incredible cool job!

The Query() method runs a query which is constructed on DTO Models, but the ProjectTo method (which is from AutoMapper) transforms the Query into an EFQuery - which is based on EF models - so it can execute the SQL on the database, and transforms the results back into DTO Models - Wow!!

Then there is a helper method which uses the FluentValidation mechanism to perform the server-side validation:

public const string CtxMode = "datamode";
public const string CtxStore = "datastore";

protected async Task<ValidationResult> ValidateDBModelAsync(TDBModel item,
    DataMode mode, 
    EFDataStore<TEFContext, TKey, TModel, TDBModel> store)
{
    var validationContext = new ValidationContext<TDBModel>(item);
    validationContext.RootContextData[CtxMode] = mode;
    validationContext.RootContextData[CtxStore] = store;
    
    var result = await Validator.ValidateAsync(validationContext);
    return result;
}

Basically only the last line await Validator.ValidateAsync(..) is needed to perform the validation but I’m putting some extra info in the validation context so we can access the DataStore and determine the operation (Update, Insert, Delete) from the Validator class.

Next there are some helper methods which allow us to execute some code in a database transaction. There are 2 - the first is able to return a value, the other one justs executes something in that transaction:

protected async virtual Task<T> TransactionalExecAsync<T>(
    Func<EFDataStore<TEFContext, TKey, TModel, TDBModel>,
    IDbContextTransaction, Task<T>> work,
    bool autoCommit = true)
{
    T result = default!;
    using (var dbTrans = await DbContext.Database.BeginTransactionAsync())
    {
        result = await work(this, dbTrans);
        if (autoCommit && DbContext.ChangeTracker.HasChanges())
        {
            await DbContext.SaveChangesAsync();
            await dbTrans.CommitAsync();
        }
    }
    return result;
}
protected async virtual Task TransactionalExecAsync<T>(
    Func<EFDataStore<TEFContext, TKey, TModel, TDBModel>,
    IDbContextTransaction, Task> work, bool autoCommit = true)
{
    using (var dbTrans = await DbContext.Database.BeginTransactionAsync())
    {
        await work(this, dbTrans);
        if (autoCommit && DbContext.ChangeTracker.HasChanges())
        {
            await DbContext.SaveChangesAsync();
            await dbTrans.CommitAsync();
        }
    }
}

The last methods in this class are the CRUD methods. You can also see how the TransactionalExecAsync methods are being used here.

public async virtual Task<IDataResult> CreateAsync(params TModel[] items)
{
    if (items == null)
        throw new ArgumentNullException(nameof(items));

    var result = await TransactionalExecAsync(async (s, t) =>
        {
            try
            {
                foreach (var item in items)
                {
                    var newItem = new TDBModel();
                    Mapper.Map(item, newItem);
                    
                    var validationResult = await ValidateDBModelAsync(newItem, DataMode.Create, s);
                    if (!validationResult.IsValid)
                        throw new ValidationException(
                            validationResult.Errors);

                    var r = await DbContext.Set<TDBModel>()
                                    .AddAsync( newItem);
                    await DbContext.SaveChangesAsync();
                    //reload changes in DTO Model
                    Mapper.Map(r.Entity, item);                     
                }
                await s.DbContext.SaveChangesAsync();
                await t.CommitAsync();
                return new EFResult { 
                    Success = true,
                    Mode = DataMode.Create 
                };
            }
            catch (Exception err)
            {
                return new EFResult(DataMode.Create, nameof(TDBModel), err);
            }
        },
        false);
    return result;
}

public async virtual Task<IDataResult> UpdateAsync(params TModel[] items)
{
    if (items == null)
        throw new ArgumentNullException(nameof(items));

    var result = await TransactionalExecAsync(async (s, t) =>
    {
        try
        {
            foreach (var item in items)
            {
                var key = ModelKey(item);
                var dbModel = EFGetByKey(key);
                if (dbModel == null)
                    throw new Exception(
                        $"Unable to locate {typeof(TDBModel).Name}({key})");
                
                Mapper.Map(item, dbModel);

                var validationResult = await ValidateDBModelAsync( 
                    dbModel, DataMode.Update, s);
                if (!validationResult.IsValid)
                    throw new ValidationException(validationResult.Errors);
                
                DbContext.Entry(dbModel).State = EntityState.Modified;
                await DbContext.SaveChangesAsync();
            }
            await s.DbContext.SaveChangesAsync();
            await t.CommitAsync();
            return new EFResult { Success = true, Mode = DataMode.Update };
        }
        catch (Exception err)
        {
            return new EFResult(DataMode.Update, nameof(TDBModel), err);
        }
    }, false);
    return result;
}

public async virtual Task<IDataResult> DeleteAsync(params TKey[] ids)
{
    if (ids == null)
        throw new ArgumentNullException(nameof(ids));

    var result = await TransactionalExecAsync(async (s, t) =>
    {
        try
        {
            foreach (var id in ids)
            {
                var dbModel = EFGetByKey(id);
                if (dbModel != null)
                {
                    var validationResult = await ValidateDBModelAsync(dbModel, DataMode.Delete, s);
                    if (!validationResult.IsValid)
                        throw new ValidationException(validationResult.Errors);

                    DbContext.Entry(dbModel).State = EntityState.Deleted;
                    await DbContext.SaveChangesAsync();
                }
            }

            await s.DbContext.SaveChangesAsync();
            await t.CommitAsync();
            return new EFResult { Success = true, Mode = DataMode.Delete };
        }
        catch (ValidationException err)
        {
            return new EFResult(DataMode.Delete, nameof(TDBModel), err);
        }
    }, false);
    return result;
}

If you take another look at this class, you might notice that all code which deals with EF model classes are made protected, while all public methods only deals with DTO Models - as parameters or results.

Refactoring the project to use our Generic DataStore

We need to prepare a couple of things before we can start using out DataStore;

  • Create a DTO Model
  • Create a FluentValidator for our EF Model

In the DxChinook.Data project, we’ll add a folder Models and in that folder, we’ll add a class CustomerModel. It looks like this:

public class CustomerModel
{
    
    public int CustomerId { get; set; }
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string? Company { get; set; }
    public string? Address { get; set; }
    public string? City { get; set; }
    public string? State { get; set; }
    public string? PostalCode { get; set; }
    public string? Country { get; set; }
    public string? Phone { get; set; }
    public string? Fax { get; set; }
    public string Email { get; set; } = null!;
}

It looks similar as the EF Customer class but you will find, when working on the project it gives you the flexibility you want later on.

Next in the DxChinook.Data.EF projects root, let’s create a file CustomerStore.cs.

I will first add a FluentValidator class for the EF Customer class:

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(x=>x.LastName).NotEmpty();
        RuleFor(x=>x.Email)
            .NotEmpty()
            .EmailAddress()
            .CustomAsync(async (email,ctx, ct)=> {                    
                if (ctx.RootContextData.ContainsKey(CustomerStore.CtxMode) &&
                    ctx.RootContextData.ContainsKey(CustomerStore.CtxStore))
                {
                    var store = (CustomerStore)ctx.RootContextData[CustomerStore.CtxStore];
                    var mode = (DataMode)ctx.RootContextData[CustomerStore.CtxMode];
                    var cust = ctx.InstanceToValidate;

                    if (await store.Query().Where(c => c.Email == email && c.CustomerId != cust.CustomerId).AnyAsync(ct))
                    {
                        ctx.AddFailure("Email address is already in use");
                    }
                }                   
            });
    }
}

Now you get an impression why this is called FluentValidator - the validation logic is coded by using a fluent syntax of one or more validation rules.

Remember I put some extra code in the ValidateDBModelAsync(..) method in our generic datastore? Because of that, I’m now able to check the store if the e-mail address is unique in the DB.

In the same file I will now code our CustomerStore:

public class CustomerStore : EFDataStore<ChinookContext, int, CustomerModel, Customer>
{

}

You will see the red whobbly line appear under CustomerStore. If you place the cursor there and hit Alt+Enter (or Ctrl + ` when using CodeRush), you’ll be able to add the missing constructor and to implement the abstract class.

The generated constructor remains as it is, and the code in the abstract methods can be filled in really simple so the Store looks like this:

public class CustomerStore : EFDataStore<ChinookContext, int, CustomerModel, Customer>
{
    public CustomerStore(ChinookContext context, 
        IMapper mapper, IValidator<Customer> validator) 
        : base(context, mapper, validator)
    {

    }

    public override string KeyField => nameof(Customer.CustomerId);

    public override int ModelKey(CustomerModel model) {
        return model.CustomerId;
    }
    
    public override void SetModelKey(CustomerModel model, int key) {
        return model.CustomerId = key;
    }
    
    protected override int DBModelKey(Customer model) {
        return model.CustomerId;
    }
}

The cool things is that all methods created by VisualStudio are now using concrete types like Customer, CustomerModel and int.

This is why I like abstract generic classes !!

In the DxChinook.Data.EF.RegisterServices class, we need to register the validator and the store:

        public static IServiceCollection RegisterDataServices(this IServiceCollection services, string connectionString)
        {
            //configure AutoMapper and EF Context
            services.AddAutoMapper(cfg => cfg.AddProfile<ChinookMappingProfile>());
            services.AddDbContext<ChinookContext>(options => 
                options.UseSqlServer(connectionString), ServiceLifetime.Transient);
            //more services here...
            services.AddScoped<IDataStore<int, CustomerModel>, CustomerStore>();
            services.AddScoped<IValidator<Customer>, CustomerValidator>();
            return services;
        }

Ready to slice some code

Now we can go back to the Customers.razor file and make some modifications to it: First, let’s remove the @using DxChinook.Data.EF.Models from the page, and replace the @inject ChinookContext ctx with @inject IDataStore<int, CustomerModel> Store

Next we can do a search and replace Customer -> CustomerModel.

(make sure you untick the UseRegular Expressions box)

Also, we need to change the methods below need to use the Store:

 protected override void OnInitialized()
{
    var dataSource = new GridDevExtremeDataSource<CustomerModel>(Store.Query());
    dataSource.CustomizeLoadOptions = (loadOptions) =>
    {
        // If underlying data is a large SQL table, specify PrimaryKey and PaginateViaPrimaryKey.
        // This can make SQL execution plans more efficient.
        loadOptions.PrimaryKey = new[] { Store.KeyField };
        loadOptions.PaginateViaPrimaryKey = true;
    };
    Data = dataSource;
}

ValidationException? serverError = default!;
async Task Grid_EditModelSaving(GridEditModelSavingEventArgs e)
{
    serverError = default!;
    var item = (CustomerModel)e.EditModel;
    var result = (e.IsNew)
        ? await Store.CreateAsync(item)
        : await Store.UpdateAsync(item);
    if (!result.Success)
    {
        e.Cancel = true;
        serverError = result.Exception as ValidationException;
    }
}

async Task Grid_DataItemDeleting(GridDataItemDeletingEventArgs e)
{
    serverError = null;
    var item = (CustomerModel)e.DataItem;
    var result = await Store.DeleteAsync(Store.ModelKey(item));
    if (!result.Success)
    {
        e.Cancel = true;
        serverError = result.Exception as ValidationException;
    }
}

You might also notice that the type of serverError has changed from string to ValidationException. Make sure to include @using FluentValidation.

To display validation error correctly in the UI, we need to change the markup in the <DxFormLayoutItem> containing the serverError.

<DxFormLayoutItem Caption="" ColSpanSm="12" BeginRow="true"
                              CssClass="formlayout-validation-summary">
    <Template>
        @if (serverError != null)
        {
            <div class="validation-errors" style="color: red;">
                <ul class="validation-errors">
                    @foreach (var e in serverError.Errors)
                    {
                        <li>@e.ErrorMessage</li>
                    }
                </ul>
            </div>
        }
    </Template>
</DxFormLayoutItem>

To wrap things up, we can change the <DxGrid KeyFieldName> declaration:

<DxGrid Data="@Data"
        ...
        KeyFieldName="@Store.KeyField">
    ...
</dxGrid>

With this in place, we are ready to run the app! The only visual thing that has changed is the error message when one of the validation rules fails.

Under the hood of the app, we have done some serious changes:

  • We have completely decoupled the EF Models from the UI (The blazor app doesn’t need to have any references to EntityFramework packages)
  • The UI is now dealing with DTO Model classes
  • We have server-side validation logic in place

The code for these changes can be downloaded from the Github repo. I have created a separate branch for this.

Make sure to read the next post. I will show you one of the things why Blazor makes me happy. I we'll reduce even more code from the Customers.razor page and add DTO Model (Front-end) validation as well.


Viewing all articles
Browse latest Browse all 2370

Trending Articles