Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entity Framework Core Support #14109

Merged
merged 35 commits into from
May 12, 2023
Merged

Entity Framework Core Support #14109

merged 35 commits into from
May 12, 2023

Conversation

Zeegaan
Copy link
Member

@Zeegaan Zeegaan commented Apr 17, 2023

Notes

  • Moved some of the core logic of scopes into a CoreScope class.
  • Implemented new IEFCoreScope which now only has logic for the database part, and the rest is derived from the CoreScope.
  • EFCore scopes now use the of T pattern, so you can bring your own context. This enables scoping for requests with custom db contexts 🚀
  • Added faking underneath, so IEFCore scope creates a infrastructure scope and uses that transaction, this way we can roll back transactions when using both Infrastructure scopes & EFCore scopes 👍
  • Created AddUmbracoEFCoreContext extension method to make it easier to register your own DbContext

How to test

Setup

  • Install umbraco (lets use SqlLite for this example) and stop umbraco afterwards.
  • Create your own DbContext with a table (Look at MyDbContext and Person code snippets underneath.)
  • Register your DbContext in Startup.cs, here is an example using the local sqlite database:
            services.AddUmbracoEFCoreContext<MyDbContext>("Data Source={INSERT-DATABASE-PATH-HERE}\\Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", "Microsoft.Data.Sqlite");

Hint: The database file when using the source code is in the Umbraco/Data folder

Migration

  • Now that we've created the models for the database, we need to create these tables in the database. This is done via a migration.
  • You can manually create these migrations by hand, or you can add the Microsoft.EntityFrameworkCore.Design to your project and autogenerate it, which is what we'll do for convenience here.
  • As described above, add the Microsoft.EntityFrameworkCore.Design package to your project.
  • Open a terminal in your project folder (The Umbraco.Web.UI folder if you're running the source code)
  • Install the EF migrations CLI tool by running dotnet tool install --global dotnet-ef
  • Run dotnet ef migrations add InitialCreate --context MyDbContext to generate the migrations.
  • We just need to run our migrations now, as this doesn't happen automatically, we'll do this in a composer (MyComposer in code snippets)

Actual testing 🥳

  • We can now implement the DemoController from the code snippets.
  • Assert that you can CRUD on a person.
  • Assert that if you do not complete the scope, that the transaction is rolled back.
  • Assert that if you create a person in a child scope it works
  • Assert that if either the parent/child scope is not completed that the transaction is rolled back.
  • Last but not least, create another DbContext, and assert that you can have multiple, repeat tests above with both mixed

Code snippets

MyDbContext.cs :

using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Web.UI.EFContext;

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
    {
    }

    public required DbSet<Person> Persons { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>(entity =>
        {
            entity.ToTable("person");
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Id).HasColumnName("id");
            entity.Property(e => e.FirstName).HasColumnName("firstName");
            entity.Property(e => e.LastName).HasColumnName("lastName");
            entity.Property(e => e.Email).HasColumnName("email");
        });
    }
}

Person.cs

namespace Umbraco.Cms.Web.UI.EFContext;

public class Person
{
    public int Id { get; set; }

    public required string FirstName { get; set; }

    public required string LastName { get; set; }

    public required string Email { get; set; }
}

MyComposer.cs

using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Web.UI.Repository;

public class MyComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder) => builder.AddNotificationHandler<UmbracoApplicationStartedNotification, MyNotHandler>();
}

public class MyNotHandler : INotificationHandler<UmbracoApplicationStartedNotification>
{
    private readonly MyDbContext _myDbContext;
    private readonly IRuntimeState _runtimeState;

    public MyNotHandler(MyDbContext myDbContext, IRuntimeState runtimeState)
    {
        _myDbContext = myDbContext;
        _runtimeState = runtimeState;
    }

    public void Handle(UmbracoApplicationStartedNotification notification)
    {
        if (_runtimeState.Level != RuntimeLevel.Run)
        {
            return;
        }

        if (_myDbContext.Database.GetPendingMigrations().Any())
        {
            _myDbContext.Database.Migrate();
        }
    }
}

DemoController.cs

using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Persistence.EFCore.Scoping;

namespace Umbraco.Cms.Web.UI.EFContext;

[ApiController]
[Route("[controller]")]
public class DemoController : ControllerBase
{
    private readonly MyDbContext _dbContext;
    private readonly IEFCoreScopeProvider<MyDbContext> _efCoreScopeProvider;

    public DemoController(MyDbContext dbContext, IEFCoreScopeProvider<MyDbContext> efCoreScopeProvider)
    {
        _dbContext = dbContext;
        _efCoreScopeProvider = efCoreScopeProvider;
    }

    [HttpGet("[action]")]
    public IActionResult ById(int id)
    {
        Person person = _dbContext.Persons.First(x => x.Id == id);
        return Ok(person);
    }

    [HttpGet]
    public IActionResult GetAll()
    {
        Person[] persons = _dbContext.Persons.ToArray();
        return Ok(persons);
    }

    [HttpPost]
    public async Task<IActionResult> Create(Person person)
    {
        using IEfCoreScope<MyDbContext> scope = _efCoreScopeProvider.CreateScope();
        await scope.ExecuteWithContextAsync<Task>(async db =>
        {
            db.Persons.Add(person);
            await db.SaveChangesAsync();
        });
        scope.Complete();
        return Ok();
    }
}

@Zeegaan Zeegaan marked this pull request as ready for review April 18, 2023 09:04
@bergmania bergmania changed the title V12: EFCore Entity Framework Core Support Apr 19, 2023
@bergmania bergmania merged commit 487e85c into v12/dev May 12, 2023
@bergmania bergmania deleted the v12/feature/ef-core branch May 12, 2023 07:25
@bergmania
Copy link
Member

Tests out as expected 💪

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants