Nano ASP.NET SaaS Boilerplate
Admin credentials (all tenants): admin@email.com / Password123!
Sample data resets every hour

Version 1.6

Ardalis Specification

The biggest change in this update is the implementation of the Ardalis Specification package. The Ardalis Specification package is a more robust specification pattern solution and includes some methods which were not covered before like ThenInclude and IgnoreQueryFilters. Follow the link to read more about Ardalis specification. All the custom specification related classes have been removed.

If you coming from a previous version, install the Ardalis.Specification Nuget package on both the Application and Infrastructure projects. On the Infrastructure project, install the Ardalis.Specification.EntityFrameworkCore Nuget package. Remove any specification classes in Application/Common

The Ardalis Specification package includes its own repository but its rather limited and the Nano boilerplate doesn't use it. Some changes occur in RepositoryAsync. Here is a side by side comparison of athe GetListAsync method, follow along for any method that uses specification:

Version 1.5

 IQueryable<T> query = _context.Set<T>();
 if (specification != null)
 {
     query = SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), specification);
 }

 List<T> result = await query.ToListAsync(cancellationToken);
 return result;

Version 1.6

IQueryable<T> query;
if (specification == null)
{
    query = _context.Set<T>().AsQueryable();
}
else
{
    query = SpecificationEvaluator.Default.GetQuery(
      query: _context.Set<T>().AsQueryable(),
      specification: specification);
}
List<T> result = await query.ToListAsync(cancellationToken);
return result;

The Product Service contains a Specifications folder for specification classes with query criteria. Here is an example of how to use Ardalis specification:

ProductSearchList specification = new(filter?.Keyword); // ardalis specification
IEnumerable list = await _repository.GetListAsync(specification); // entity mapped to dto
return Response<ProductDto>.Success(list);

With ProductSearchList like this:

public class ProductSearchList : Specification<Product>
{
    public ProductSearchList(string? name = "", string? dynamicOrder = "")
    {

        // filters
        if (!string.IsNullOrWhiteSpace(name))
        {
            Query.Where(x => x.Name.Contains(name));
        }


        // sort order
        if (string.IsNullOrEmpty(dynamicOrder))
        {
            Query.OrderByDescending(x => x.CreatedOn); // default sort order
        }
        else
        {
            Query.OrderBy(dynamicOrder); // dynamic (JQDT) sort order
        }


    }
}

Ardalis Specification doesn't contain any method to dynamically sort columns, like in the case of Datatables, a user can select columns and even specify multiple column sort criteria by shift + clicking on a column. To accommodate this, we've added an extension, ArdalisSpecificationExtensions, which is found under Application/Common/Specification/. This extension allows you to pass in a string sort expression to the OrderBy method.

Helper methods in Application/Utility/NanoHelpers translates JQuery Datatables and Tanstack Table v8 pagination format to a generic string for OrderBy extension to parse.

Automapper ProjectTo vs Map

In RepositoryAsync we can use ProjectTo instead of running a query with Map to return DTOs more efficiently. To compare:

Version 1.5

List list = await query.ToListAsync(cancellationToken);
List result = _mapper.Map>(list); // map results

Version 1.6

List result = await query
.ProjectTo(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);

The latter has better performance and does a better job mapping related entities. This post is an excellent explanation for understanding how ProjectTo works in Automapper.

Soft Delete Changes

The last change in this update deals with soft deletion. In practice, soft delete is probably not the default behavior you want. One of the biggest drawbacks of soft delete is that it doesn't allow for cascade delete.

For this reason, a new abstract class has been added in Domain/Common which is the AuditableEntityWithSoftDelete. That class now contains the soft delete fields and implements ISoftDelete and AuditableEntity does not include them. The only entity to implement soft delete behavior now is the ApplicationUser. In cases where you do need soft delete, inherit from AuditableEntityWithSoftDelete and be mindful about what happens to related entities.

Notes

We will release notes like this moving forward for any updates to the boilerplate code. Thank you