ASP.NET Core 5.0 - Framework Enum

Ken Haggerty
Created 10/12/2020 - Updated 10/12/2020 14:02
NET 5 Release Canidate versions require Visual Studio 2019 v16.8.0 Preview 3 or above.

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

I implemented a TargetFrameworkEnum 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 Target Framework property to projects and refactor the selection and filtering of articles by Target Framework.

I want to add new ASP.NET Core 5.0 projects side by side with ASP.NET Core 3.1 projects. I removed ASP.NET Core 2.2 projects which had reached EOS (End Of Support). ASP.NET Core 3.1 is a LTS (Long Term Service) release with support till December 3, 2022. After working on the implementation of a Target Framework for projects, I realized I needed to do a better job with the TargetFrameworkEnum. I started with renaming the TargetFrameworkEnum to FrameworkEnum. My article entity had a TargetFrameworkEnum property named TargetFramework. For continuity, I renamed the article property to Framework and ran an add-migration command.

public partial class FrameworkUpdate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "TargetFramework",
            schema: "article",
            table: "Articles");

        migrationBuilder.AddColumn<int>(
            name: "Framework",
            schema: "article",
            table: "Articles",
            type: "int",
            nullable: false,
            defaultValue: 0);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "Framework",
            schema: "article",
            table: "Articles");

        migrationBuilder.AddColumn<int>(
            name: "TargetFramework",
            schema: "article",
            table: "Articles",
            type: "int",
            nullable: false,
            defaultValue: 0);
    }
}

Notice an Enum is stored in the database as an int type. This migration doesn't rename the column, it drops the column and my data then adds a new column with a default value = 0. Research found: Entity Framework Migrations renaming tables and columns.

public partial class FrameworkUpdate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.RenameColumn(name: "TargetFramework", table: "Articles",
            newName: "Framework", schema: "article");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.RenameColumn(name: "Framework", table: "Articles",
            newName: "TargetFramework", schema: "article");
    }
}

FrameworkEnum with Net 5

public enum FrameworkEnum
{
    Any,
    [Description("ASP.NET Core 2.2")]
    Core2,
    [Description("ASP.NET Core 3.1")]
    Core3,
    [Description("ASP.NET Core 5.0 RC1")]
    Net5
}

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 EnumExtentions
{
    public static string GetDescription(this Enum enumSrc)
    {
        Type srcType = enumSrc?.GetType();
        MemberInfo[] memberInfo = srcType. GetMember(enumSrc.ToString());
        if ((memberInfo != null && memberInfo.Length > 0))
        {
            var _Attribs = memberInfo[0]. GetCustomAttributes(typeof(DescriptionAttribute), false);
            if (_Attribs != null && _Attribs.Length > 0)
                return ((DescriptionAttribute)_Attribs. ElementAt(0)). Description;
        }
        return src.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 mr-2" method="get">
            <label asp-for="@@Model.Framework" class="control-label font-weight-bold mt-2 mr-2"></label>
            <select class="form-control align-top mr-2" style="min-width:12rem;" asp-for="@@Model.Framework"
                    asp-items="@@Model.FrameworkSelectList" onchange="this.parentElement.submit();">
            </select>
        </form>
    </div>
</div>

Notice the form's method="get" and the select's onchange="this.parentElement.submit();". To compose this article, I developed a research project which implements an in-memory database seeded with mock articles.

Article.cs:
public class Article
{
    [Key]
    public int Id { get; set; }
    public int AuthorId { get; set; }
    public string Name { get; set; }
    public FrameworkEnum Framework { get; set; }
}

The database requires a DbContext and NuGet packages, Microsoft. EntityFrameworkCore and Microsoft. EntityFrameworkCore. InMemory.

ArticleDbContext.cs:
public class ArticleDbContext : DbContext
{
    public ArticleDbContext(DbContextOptions<ArticleDbContext> options)
        : base(options)
    {
    }
    public DbSet<Article> Articles { get; set; }
}

The database is seeded with a HostedService.

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();
            }
            await articleDbContext.SaveChangesAsync(cancellationToken);
        }
    }

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

Add ArticleDbContext and EnsureSampleData to the services collection.

Startup.ConfigureServices:
services.AddDbContext<ArticleDbContext>(options => options. UseInMemoryDatabase(databaseName: "Articles"));
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 IList<Article> ArticleList { get; set; } = new List<Article>();

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

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 List<SelectListItem>();
    public IList<Article> ArticleList { get; set; } = new List<Article>();

    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.

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

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();
}

Notice I prepend an Any option to AuthorSelectList.

To demonstrate Frameworks filtered by the selected Author, we need to expand the sample data. Add a new Framework to the FrameworkEnum.

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

Skip the FrameworkEnum.Net6 when EnsureSampleData generates the first set of mock articles then add a set with a new AuthorId and Net6.

Update EnsureSampleData.cs:
for (i = 0; i < 30; ++i)
{
    articleDbContext.Articles.Add(new Article()
    {
        AuthorId = i % 3 + 1,
        Name = $"{framework.GetDescription()} Article",
        Framework = framework
    });                    
    framework = framework.GetNext();

    if (framework == FrameworkEnum.Net6)
        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);

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.

Update 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();

I created a research project to compose this article. Registered users on KenHaggerty.Com can download the source code for free at Manage > Assets. KenHaggerty.Com implements this project. I created a topic, ASP.NET Core 5.0 - Framework Enum Project for discussions. More details and screenshots at ASP.NET Core 5.0 - Framework Enum Project.

Filter Framework By Author
Filter Framework By Author Mobile

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications