ASP.NET Core 6.0 - Framework Enum

Ken Haggerty
Created 10/29/2021 - Updated 10/29/2021 00:39

This article describes the implementation of a Framework Enum with a Dropdown list to filter an in-memory database. I will assume you have created a new ASP.NET Core 6.0 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core.

I implemented a FrameworkEnum on KenHaggerty.Com when ASP.NET Core 3.0 was released. With the release of NET 5 and ASP.NET Core 5.0, I decided to add a Framework property to projects and refactor the selection and filtering of articles by Framework. I developed the ASP.NET Core 5.0 - Framework Enum Project which implements an in-memory database seeded with mock articles to demonstrate. I updated the GetDescription Enum extension using the generic type parameter and added the NET 6 value and description.

The project has been upgraded to ASP.NET Core 6.0 with Visual Studio 2022 from the ASP.NET Core 5.0 project. I migrated the Startup.cs to top-level statements in Program.cs. I also implemented global usings and file scoped namespaces. I have enabled the nullable context and mitigated all warnings and issues. I have updated the project with Twitter Bootstrap v5. See ASP.NET Core 5.0 - Migrate To Bootstrap v5. This project implements Bootstrap Native. Details, screenshots, and related articles can be found at ASP.NET Core 6.0 - Framework Enum Project. The details page includes the version change log. Registered users on KenHaggerty.Com can download the source code for free at Manage > Assets.

To implement the in-memory database, install the Microsoft. EntityFrameworkCore and Microsoft. EntityFrameworkCore. InMemory NuGet packages. The database requires a DbContext and a DbSet of Article. Notice the Name property is non nullable and initialized with string.Empty. Also notice the new syntax to initialize the DbSet when the nullable context is enabled. See MSDocs - Working with Nullable Reference Types - DbContext and DbSet.

Article.cs:
public class Article
{
    [Key]
    public int Id { get; set; }
    public int AuthorId { get; set; }
    public string Name { get; set; } = string.Empty;
    public FrameworkEnum Framework { get; set; }
}
ArticleDbContext.cs:
public class ArticleDbContext : DbContext
{
    public ArticleDbContext(DbContextOptions<ArticleDbContext> options)
        : base(options)
    {
    }
    public DbSet<Article> Articles => Set<Article>();
}

The database is seeded with a HostedService. To demonstrate Frameworks filtered by the selected Author, we need to skip the FrameworkEnum.Net6 when generating the first set of mock articles then add a set with a new AuthorId and Net6.

EnsureSampleData.cs:
public class EnsureSampleData : IHostedService
{
    private readonly IServiceProvider _serviceProvider;
    public EnsureSampleData(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        using var scope = _serviceProvider. CreateScope();
        using var articleDbContext = scope. ServiceProvider. GetRequiredService<ArticleDbContext>();
        if (!articleDbContext.Articles.Any()) // Not seeded.
        {
            FrameworkEnum framework = FrameworkEnum.Any;
            int i;
            for (i = 0; i < 30; ++i)
            {
                articleDbContext.Articles.Add(new Article()
                {
                    AuthorId = i % 3 + 1,
                    Name = $"{framework.GetDescription()} Article",
                    Framework = framework
                });
                framework = framework.GetNext();
            }

            framework = FrameworkEnum.Net6;
            for (i = 0; i < 5; ++i)
            {
                articleDbContext.Articles.Add(new Article()
                {
                    AuthorId = 4,
                    Name = $"{framework.GetDescription()} Article",
                    Framework = framework
                });
            }

            await articleDbContext.SaveChangesAsync(cancellationToken);
        }
    }

    // noop
    public Task StopAsync(CancellationToken cancellationToken) => Task. CompletedTask;
}

Add ArticleDbContext and EnsureSampleData to the services collection.

Program.cs:
var webApplicationBuilder = WebApplication.CreateBuilder(args);
webApplicationBuilder.Services.AddDbContext<ArticleDbContext>(options => options. UseInMemoryDatabase(databaseName: "Articles"));
webApplicationBuilder.Services.AddHostedService<EnsureSampleData>();

Use dependency injection to access the article data in a page.

Index.cshtml.cs:
public class IndexModel : PageModel
{
    private readonly ArticleDbContext _articleDbContext;
    public IndexModel(ArticleDbContext articleDbContext)
    {
        _articleDbContext = articleDbContext;
    }
    public List<Article> ArticleList { get; set; } = new();

    public async Task OnGetAsync()
    {
        ArticleList = await _articleDbContext.Articles
            .AsNoTracking()
            .ToListAsync();
    }
}

An enum type is a distinct value type (Value types) that declares a set of named constants. An enum declaration that does not explicitly declare an underlying type has an underlying type of int. See MSDocs - Enums. The Article's Framework property is stored in the database as an int type. Add the FrameworkEnum with the new Net6 value and new descriptions.

FrameworkEnum.cs:
public enum FrameworkEnum
{
    Any,
    [Description("ASP.NET Core 2.2")]
    Core2,
    [Description("ASP.NET Core 3.1 LTS")]
    Core3,
    [Description("ASP.NET Core 5.0")]
    Net5,
    [Description("ASP.NET Core 6.0 LTS")]
    Net6
}

Notice the Description attributes. The description can be updated without impacting the stored data. Early on, I found Getting attributes of Enum's value and implemented a GetDescription extension which returns the Enum value if the Description attribute is not present. The GetNext extension is used to populate the sample data in the research project. See How to get next (or previous) enum value in C#.

public static class EnumExtensions
{
    public static string GetDescription<T>(this T enumSrc) where T : Enum
    {
        FieldInfo? fieldInfo = enumSrc.GetType().GetField(enumSrc.ToString());
        if (fieldInfo == null) return enumSrc.ToString();
        DescriptionAttribute[] attributes = (DescriptionAttribute[])fieldInfo
            .GetCustomAttributes(typeof(DescriptionAttribute), false);
        if (attributes != null && attributes.Length > 0) 
            return attributes[0].Description;
        else return enumSrc.ToString();
    }

    public static T GetNext<T>(this T enumSrc) where T : struct
    {
        if (!typeof(T).IsEnum)
            throw new ArgumentException(string.Format("Argument {0} is not an Enum", typeof(T).FullName));
        T[] Arr = (T[])Enum.GetValues(enumSrc.GetType());
        int j = Array.IndexOf<T>(Arr, enumSrc) + 1;
        return (Arr.Length == j) ? Arr[0] : Arr[j];
    }
}

I implement a Framework select on the listing pages.

Index.cshtml.cs:
public class IndexModel : PageModel
{
    public int Framework { get; set; }
    public List<SelectListItem> FrameworkSelectList { get; set; } = new List<SelectListItem>();
    public void OnGet()
    {
        foreach (FrameworkEnum frameworkEnum in Enum. GetValues(typeof(FrameworkEnum)))
            FrameworkSelectList.Add(new SelectListItem() { Text = frameworkEnum. GetDescription(), Value = ((int)frameworkEnum). ToString() });
    }
}
Index.cshtml:
<div class="row mb-1">
    <div class="col-12">
        <form class="d-inline-flex me-2" method="get">
            <label asp-for="@@Model.Framework" class="control-label fw-bold mt-2 me-2"></label>
            <select class="form-control align-top me-2" style="min-width:12rem;" asp-for="@@Model.Framework"
                    asp-items="@@Model.FrameworkSelectList" onchange="this.form.submit();">
            </select>
        </form>
    </div>
</div>

Filter the data by adding a framework parameter with a default value = 0 to the listing page's OnGetAsync() method and add a Where clause to the query.

Index.cshtml.cs:
public class IndexModel : PageModel
{
    private readonly ArticleDbContext _articleDbContext;
    public IndexModel(ArticleDbContext articleDbContext)
    {
        _articleDbContext = articleDbContext;
    }

    public int Framework { get; set; }
    public List<SelectListItem> FrameworkSelectList { get; set; } = new();
    public List<Article> ArticleList { get; set; } = new();

    public async Task OnGetAsync(int framework = 0)
    {
        Framework = framework;
        foreach (FrameworkEnum frameworkEnum in Enum. GetValues(typeof(FrameworkEnum)))
            FrameworkSelectList.Add(new SelectListItem() { Text = frameworkEnum. GetDescription(), Value = ((int)frameworkEnum). ToString() });

        ArticleList = await _articleDbContext. Articles
            .AsNoTracking()
            .Where(a => Framework > 0 ? (int)a.Framework == Framework : a.Framework == a.Framework)
            .ToListAsync();
    }
}

Notice the Where predicate implements the expected behavior of FrameworkEnum.Any. If you add a select filter on Article.AuthorId, you should list only the AuthorIds in the list of articles filtered by Framework. Notice I prepend an Any option to AuthorSelectList.

Index.cshtml.cs:
[Display(Name = "Author")]
public int AuthorId { get; set; }
public List<SelectListItem> AuthorSelectList { get; set; } = new();

public async Task OnGetAsync(int framework = 0, int authorId = 0)
{
    Framework = framework;
    AuthorId = authorId;

    foreach (FrameworkEnum frameworkEnum in Enum. GetValues(typeof(FrameworkEnum)))
        FrameworkSelectList.Add(new SelectListItem() { Text = frameworkEnum.GetDescription(), Value = ((int)frameworkEnum).ToString() });

    var articlesQuery = _articleDbContext.Articles
        .AsNoTracking()
        .Where(a => Framework > 0 ? (int)a.Framework == Framework : a.Framework == a.Framework);

    ArticleList = await articlesQuery.ToListAsync();

    AuthorSelectList = await articlesQuery
        .Select(a => a.AuthorId)
        .Distinct()
        .OrderBy(a => a)
        .Select(a => new SelectListItem() { Text = $"Author {a}", Value = a.ToString() })
        .ToListAsync();

    if (!FrameworkSelectList.Any(s => s.Value == "0"))
        AuthorSelectList = AuthorSelectList. Prepend(new SelectListItem() { Text = "Any", Value = "0" }).ToList();
}

Filter the article list by Framework and AuthorId. Then query the article list for distinct Frameworks and AuthorIds. An author might have an unassigned article with a FrameworkEnum.Any value. If not add an Any option.

Index.cshtml.cs:
var articlesQuery = _articleDbContext.Articles
    .AsNoTracking()
    .Where(a => Framework > 0 ? (int)a.Framework == Framework : a.Framework == a.Framework)
    .Where(a => AuthorId > 0 ? a.AuthorId == AuthorId : a.AuthorId == a.AuthorId);

ArticleList = await articlesQuery.ToListAsync();

FrameworkSelectList = await articlesQuery
    .Select(a => a.Framework)
    .Distinct()
    .OrderBy(a => a)
    .Select(a => new SelectListItem() { Text = a.GetDescription(), Value = ((int)a).ToString() })
    .ToListAsync();

if (!FrameworkSelectList.Any(s => s.Value == "0"))
    FrameworkSelectList = FrameworkSelectList.Prepend(new SelectListItem() { Text = "Any", Value = "0" }).ToList();

AuthorSelectList = await articlesQuery
    .Select(a => a.AuthorId)
    .Distinct()
    .OrderBy(a => a)
    .Select(a => new SelectListItem() { Text = $"Author {a}", Value = a.ToString() })
    .ToListAsync();

AuthorSelectList = AuthorSelectList.Prepend(new SelectListItem() { Text = "Any", Value = "0" }).ToList();

Article Tags:

EF Core Model SQL

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications