msawczyn / EFDesigner2022

Entity Framework visual design surface and code-first code generation for EF6, Core and beyond
MIT License
119 stars 21 forks source link

ASP.NET Core Identity support [Feature Request] #38

Open mmarinchenko opened 5 years ago

mmarinchenko commented 5 years ago

To support custom ASP.NET Core Identity scenario with EFDesigner some manual work has to be done.

  1. Implement 7 custom entities which inherit types from Microsoft.AspNetCore.Identity namespace:
    • IdentityUser
    • IdentityRole
    • IdentityUserClaim<TKey>
    • IdentityUserRole<TKey>
    • IdentityRoleClaim<TKey>
    • IdentityUserLogin<TKey>
    • IdentityUserToken<TKey>

Note: TKey type parameter defaults to string type.

  1. Implement custom part of EFDesigner-generated DbContext class to define 7 respective DbSets and create model using partial OnModelCreatedImpl() method.

  2. Somehow inherit IdentityDbContext<TUser,TRole,TKey,TUserClaim,TUserRole,TUserLogin,TRoleClaim,TUserToken> type from Microsoft.AspNetCore.Identity.EntityFrameworkCore namespace instead of default DbContext from Microsoft.EntityFrameworkCore.

Note: IdentityDbContext<> in turn inherits DbContext.

First 2 points are manual work because EFDesigner lacks:

This is safe to implement. Not a big problem actually.

But 3rd point needs to copy EFCoreDesigner.ttinclude template to a project directory and remove the : Microsoft.EntityFrameworkCore.DbContext text from it. Then in file from point 2 add something like : Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext<MyUser, MyRole, string/*TKey*/, MyUserClaim, MyUserRole, MyUserLogin, MyRoleClaim, MyUserToken>.

This introduces a problem to EFDesigner extension updates management. So it would be great to implement string property in EFDesigner for setting custom base class for DbContext (like ConnectionString). Truth to be told it's the only crusial part of all this request :)

msawczyn commented 5 years ago

Intriguing. I'll look into it. Thanks for the suggestion.

As an aside, your comment above:

... EFDesigner lacks:

- support  for generated entity to inherit any type (not just other entity type defined on a diagram);

isn't completely accurate. Since generated entities are partial classes, you can declare a base class in a custom partial file as long, of course, as there isn't any other base class for that type in your model.

mmarinchenko commented 5 years ago

Thanks, @msawczyn!

You're right, I was not completely accurate about entity inheritance. I guess I wrote too many words :) In fact I created this feature request mainly becuase of point 3.

As for the entity inheritance... This is difficult. Since the base type (e.g. IdentityUser) is not a part of a diagram the model for its properities will not be autogenerated. This requires some kind of import feature in EFDesigner. Inherited properties must be a part of a diagram and must be generated in data context class but at the same time they must be readonly on a diagram and must be excluded from entity class generation. Besides that the base class may be changed in future. So EFDesigner must check this and reimport base properties... This is difficult :)

ensemblebd commented 5 years ago

I really think this is going to need totally separate tt template generators.

Several critical factors:

The last item I think is the game changer. I'm not sure how to even approach this.

Hmm. Am speaking aloud here. Welcome any thoughts.


I truly wish I had more time to work on this aspect. Pulling hair out as it is. This extension is overly well deserving of community support on this. And imagine the possibilities with base-class inheritance + control in gui, and abstraction through generated partials adhering to above principles. You'd simply drag n' drop, and expose a scoped interface in your services. How cool.

+1 vote on feature request from me. If i can find time, I'll do what I can to contribute ideas/concept-work to help edge this along.

msawczyn commented 5 years ago

Thanks for the excellent input! Let's hit those up in order:

As always, I welcome any contributions you might have time for. I'm a bit swamped at work right now and don't have a lot of time to work in many (any?) hours for the designer, but that should change in a couple of handfuls of weeks. This is an intriguing problem!

Thanks again for the input.

ensemblebd commented 5 years ago

I was going to create a sample project, but ran out of time. The committed TT templates work for Identity if you feed it a baseClass followed by subclass (drag/drop), where base-class is a basic "class model" of decompiled AspnetCore 2.1 Identity public properties with a Identity[Superclass]<TKey>, and subClass extends said "base". https://github.com/ensemblebd/EFDesigner/commit/d49079ea2244a958e38db92f1a709b4700186bf8 And here's the .efmodel vstemplate (bypass feeding requirement, throw into project): https://github.com/ensemblebd/EFDesigner/commit/1c8ee692d72a5acd2091099bba8d95d4ab8cdd6d The commits are (I believe) unworthy of production, let alone a pull request. But provides a sample changeset for intended goal. aka "works for me, wish you luck!"

If I can find time this week, I'll submit a git repo in this issue for a working proof of concept AspNetCore project. I have it working for my project, but gotta get rid of all the company stuff to provide such a thing publicly.

  1. My custom TT makes some assumptions: (project override - aka bad for extension updates)
    • <TKey> is hardcoded both at template level and efmodel level (need an MEF prop to procure designer field)
    • DbSets were dropped for base-classes to prevent collision with Identity. I believe base-class reflection is required here (to know what is and isn't editable). I have hardcoded what is ignored.
    • Default class names are assumed and hardcoded into the template: MyNamespace.IdentityUser is derived from a base class of AspNetCore.IdentityUser<TKey> (for generator usage/purposes due to # 2 below), and is subclassed by ApplicationUser (the custom impl).
    • Default constructor for subclass must be marked public to be used in Type Parameters for Identity class registration
  2. Drag/Drop doesn't work with <TKey> , need MEF modification to support this. It tries to make use of it as a DbSet, in this case a.) no dbset is desired and b.) nor can it be textually procured given the raw text [< , > ] symbols.
  3. MEF Designer allows editing of base-class tables, which is not possible for derived AspNetCore.Identity base classes (user-error prevention). With my sample .efmodel, it's all about "hey you, don't touch that"
  4. The .efmodel attributes are important, so standardization of a .efmodel template for Identity may be necessary.

Additional /etc Not related to Identity but valuable...

  1. Some tables have multiple primary keys, which need an Order specified. I used the custom attr prop on modelClass, however it required some hackery to prevent duplicate [Key] column. Perhaps an exposed prop for "Key Order" could be used, or an improvement to prevent Validation error on tables with no primary key.

  2. Owin (anyone use that? hmm) requires access to the DbContextOptions<TClass> constructor, so added a partial for that

mmarinchenko commented 5 years ago

I performed some additional tests for ASP.NET Core scenario and realize that first 2 points of original request are not needed for default identity support :) The only thing that needs to be done is point 3.

P.S. I also mention this in issue msawczyn/EFDesigner#72.

msawczyn commented 5 years ago

Just wanted to let you know that I've started implementing this, both in EF6 and EFCore. If you'd like to follow the progress (and contribute!) the branch is called identity-context.

prince272 commented 5 years ago

I've rewritten the Asp Core Identity for Mvc. Feel free to take a look at https://github.com/prince272/AspCoreIdentityMvc

Simply change the url for the area 'Identity' to 'IdentityMvc' and you'll be automatically directed to the Mvc version for Identity.

msawczyn commented 5 years ago

Nice ... thanks! I'll dig into that. I've got the changes needed in the designer pretty much done, so the next step is the code generation. This will certainly help.

burakdobur commented 3 years ago

Hi @msawczyn , I'm also interested in this topic. Is there way to speed up things by helping you out in this issue/enhancement ?

msawczyn commented 3 years ago

Absolutely! Always happy to have the help!

The goal, obviously, is to create a mechanism where the user can easily scaffold a model that will work with asp.net identity, then be able to modify it to their needs without breaking its compatability.

There's an older branch named identity-context that I had started some time ago; the approach was having a different model starting template. Feel free to see if that's valid or needs scrapped and redone.

Thanks for volunteering.

msawczyn commented 3 years ago

Hey all, just wanted to let you know that, even after being on the list for a year, this isn't being ignored ... it just keeps getting bumped down in priority. It's not a simple task to get this right so that it can be used as a general modeling aid (will definitely require a custom starting project item) and will need some basic infrastructure before it can be implemented.

The EFCore5 release is taking up all available hours to get solid. But I haven't forgotten!

burakdobur commented 10 months ago

Greetings, I've been using your extension myself and with fellow developers. I hope changing the code base for 2022 made some room to give priority for this request. Using this great tool with built-in identity solution would makes us more than happy. Thanks again.

Mattnificent commented 9 months ago

I have been using Entity Framework Visual Editor for a few years now; it has been a great tool for quickly reasoning about my data models. However, I am accumulating tech debt in a few different projects now from lack of integration of this tool with Microsoft Identity. I first attempted to resolve this on my own here. I have since started up a few projects where I maintain 2 separate "user"/"person" tables with a 1-1 relationship. This has resulted in lots of extra files, classes, lines of code, SQL queries, and general obscurity that probably leads down paths away from best practices. Here is an example of a "best" solution I could come up with after way more time than I would like to have spent:

public partial class Person
{
    [NotMapped]
    public string Email => AspNetUser.Email;

    /// <summary>
    /// THIS MUST BE EXPLICITLY SET
    /// - it will not be auto-populated
    /// </summary>
    public IdentityUser AspNetUser { get; set; }

    public override string ToString()
    {
        return $"{this.UserName} [{this.Email}]"; //  unnecessary: ({this.Id}, {this.AspNetUserId})
    }
}

And then on the Razor index page:

public class IndexModel : PageModel
{
    private readonly MyModel _context;
    private readonly UserManager<IdentityUser> _userManager;
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(MyModel context, UserManager<IdentityUser> userManager,
        ILogger<IndexModel> logger)
    {
        _context = context;
        _userManager = userManager;
        _logger = logger;
    }

    public List<Person> people { get;set; } = default!;

    public async Task OnGetAsync()
    {
        List<IdentityUser> allUsers = await _userManager.Users
            .ToListAsync();

        people = await _context.People
            .ToListAsync();

        foreach (var person in people)
        {
            person.AspNetUser = allUsers
                .Where(x => x.Id == person.AspNetUserId)
                .First();
        }
    }
}

Does anyone have any better workarounds, or solutions for this disconnection of EF Visual Editor from Microsoft's Identity model?

Mattnificent commented 9 months ago

And, the code I just posted above has broken another line of code where I was doing:

person.Email = User.FindFirstValue(ClaimTypes.Email);

in another Razor page. I will have to write even more logic to gracefully handle when I have the User ClaimsPrincipal, but not the IdentityUser - in all of my projects. I google search get IdentityUser from ClaimsPrincipal, and there are no results - this is a big red flag for me, and I am very concerned about the viability of my projects being designed with this tool now.

mmarinchenko commented 9 months ago
/// <summary>
/// THIS MUST BE EXPLICITLY SET
/// - it will not be auto-populated
/// </summary>
public IdentityUser AspNetUser { get; set; }

@Mattnificent, the data context class generated from the EF model is partial. It has partial methods for customization purposes:

    partial void CustomInit(DbContextOptionsBuilder optionsBuilder);

    partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
    partial void OnModelCreatedImpl(ModelBuilder modelBuilder);

Therefore, in this particular case, you can manually add the \<YourEFModelName>.custom.cs file with the following text:

public partial class <YourEFModelName>
{
    partial void OnModelCreatedImpl(ModelBuilder modelBuilder) =>
        modelBuilder.Entity<Person>()
            .HasOne<IdentityUser>(p => p.AspNetUser)
            .WithOne();
}

This adds the Person.AspNetUser property to the model as navigation property with a One-to-One association.

Do not forget to generate a new migration. And check the Context Base Class property of the EF model - it should be Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext.

Hope this helps!😉

mmarinchenko commented 9 months ago

I google search get IdentityUser from ClaimsPrincipal, and there are no results

I guess you can use var aspNetUser = await _userManager.GetUserAsync(claimsPrincipal);

Mattnificent commented 9 months ago

@mmarinchenko Thank you!!! You are a saint, and a king! I had tried to use the ForeignKey("AspNetUserId") attribute on the custom AspNetUser property to get the context to understand the 1-1 relationship between those tables, but it gave me some strange constructor error, so I assumed it was not possible. The Fluent API approach did the trick. Even the database deployment didn't have to drop any data, and my new OnGetAsync method worked first try out of the box:

public async Task OnGetAsync()
{
    people = await _context.People
        .Include(x => x.AspNetUser)
        .ToListAsync();
}

If you ever need a favor, I'm your guy.