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

[ODS-6215] Add EF Core DbContexts and supporting code to EdFi.Admin.DataAccess (5.x) #1079

Merged
merged 11 commits into from
Jun 18, 2024
4 changes: 0 additions & 4 deletions .github/workflows/Lib edFi.admin.dataaccess pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ jobs:

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: 3.1.x
- name: build
run: |
.\build.githubactions.ps1 build -Configuration ${{ env.CONFIGURATION }} -InformationalVersion ${{ env.INFORMATIONAL_VERSION}} -BuildCounter ${{ github.run_number }} -BuildIncrementer ${{env.BUILD_INCREMENTER}} -Solution "Application/EdFi.Admin.DataAccess/EdFi.Admin.DataAccess.sln" -ProjectFile "Application/EdFi.Admin.DataAccess/EdFi.Admin.DataAccess.csproj"
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/Lib edFi.admin.dataaccess.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ jobs:

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: 3.1.x
- name: build
run: |
.\build.githubactions.ps1 build -Configuration ${{ env.CONFIGURATION }} -InformationalVersion ${{ env.INFORMATIONAL_VERSION}} -BuildCounter ${{ github.run_number }} -BuildIncrementer ${{env.BUILD_INCREMENTER}} -Solution "Application/EdFi.Admin.DataAccess/EdFi.Admin.DataAccess.sln" -ProjectFile "Application/EdFi.Admin.DataAccess/EdFi.Admin.DataAccess.csproj"
Expand Down
30 changes: 17 additions & 13 deletions Application/EdFi.Admin.DataAccess/Contexts/IUsersContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,43 @@

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Threading;
using System.Threading.Tasks;
using EdFi.Admin.DataAccess.Models;
using Microsoft.EntityFrameworkCore;

namespace EdFi.Admin.DataAccess.Contexts
{
public interface IUsersContext : IDisposable
{
IDbSet<User> Users { get; set; }
DbSet<User> Users { get; set; }

IDbSet<ApiClient> Clients { get; set; }
DbSet<ApiClient> Clients { get; set; }

IDbSet<ClientAccessToken> ClientAccessTokens { get; set; }
DbSet<ClientAccessToken> ClientAccessTokens { get; set; }

IDbSet<Vendor> Vendors { get; set; }
DbSet<Vendor> Vendors { get; set; }

IDbSet<Application> Applications { get; set; }
DbSet<Application> Applications { get; set; }

IDbSet<Profile> Profiles { get; set; }
DbSet<Profile> Profiles { get; set; }

IDbSet<OdsInstance> OdsInstances { get; set; }
DbSet<OdsInstance> OdsInstances { get; set; }

IDbSet<OdsInstanceComponent> OdsInstanceComponents { get; set; }
DbSet<OdsInstanceComponent> OdsInstanceComponents { get; set; }

IDbSet<ApplicationEducationOrganization> ApplicationEducationOrganizations { get; set; }
DbSet<ApplicationEducationOrganization> ApplicationEducationOrganizations { get; set; }

IDbSet<VendorNamespacePrefix> VendorNamespacePrefixes { get; set; }
DbSet<VendorNamespacePrefix> VendorNamespacePrefixes { get; set; }

IDbSet<OwnershipToken> OwnershipToken { get; set; }
DbSet<OwnershipToken> OwnershipTokens { get; set; }

DbSet<ApiClientOwnershipToken> ApiClientOwnershipTokens { get; set; }


int SaveChanges();

Task<int> SaveChangesAsync();
Task<int> SaveChangesAsync(CancellationToken cancellationToken);

/// <summary>
/// Asynchronously executes a raw SQL statement with only a scalar result (e.g. row count).
Expand Down
66 changes: 15 additions & 51 deletions Application/EdFi.Admin.DataAccess/Contexts/PostgresUsersContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,32 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using EdFi.Admin.DataAccess.Extensions;
using EdFi.Admin.DataAccess.Models;
using EdFi.Common;
using EdFi.Common.Utils.Extensions;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace EdFi.Admin.DataAccess.Contexts
{
public class PostgresUsersContext : UsersContext
{
public PostgresUsersContext(string connectionString) : base(connectionString) { }
public PostgresUsersContext(DbContextOptions options)
: base(options) { }

protected override void ApplyProviderSpecificMappings(DbModelBuilder modelBuilder)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// The column name in this linking table had to be shortened for Postgres
modelBuilder.Entity<ApiClient>()
.HasMany(t => t.ApplicationEducationOrganizations)
.WithMany(t => t.Clients)
.Map(
m =>
{
m.ToTable("ApiClientApplicationEducationOrganizations", "dbo");
m.MapLeftKey("ApiClient_ApiClientId");
m.MapRightKey("ApplicationEdOrg_ApplicationEdOrgId");
});
base.OnModelCreating(modelBuilder);

modelBuilder.Conventions.Add<ForeignKeyLowerCaseNamingConvention>();
modelBuilder.Conventions.Add<TableLowerCaseNamingConvention>();
modelBuilder.Model.GetEntityTypes().ForEach(
entityType =>
entityType.SetSchema("dbo"));

modelBuilder.Properties().Configure(c => c.HasColumnName(c.ClrPropertyInfo.Name.ToLowerInvariant()));
}

private class TableLowerCaseNamingConvention : IStoreModelConvention<EntitySet>
{
public void Apply(EntitySet entitySet, DbModel model)
{
Preconditions.ThrowIfNull(entitySet, nameof(entitySet));
Preconditions.ThrowIfNull(model, nameof(model));

entitySet.Table = entitySet.Table.ToLowerInvariant();
}
}

private class ForeignKeyLowerCaseNamingConvention : IStoreModelConvention<AssociationType>
{
public void Apply(AssociationType association, DbModel model)
{
Preconditions.ThrowIfNull(association, nameof(association));
Preconditions.ThrowIfNull(model, nameof(model));

if (!association.IsForeignKey)
{
return;
}

association.Constraint.FromProperties.ForEach(PropertyNamesToLowerInvariant);
association.Constraint.ToProperties.ForEach(PropertyNamesToLowerInvariant);
modelBuilder.Model.GetEntityTypes().Single(e => e.ClrType.Name == nameof(ApiClientApplicationEducationOrganization))
.GetProperty("ApplicationEducationOrganizationId")
.SetColumnName("applicationedorg_applicationedorgid");

void PropertyNamesToLowerInvariant(EdmProperty property) => property.Name = property.Name.ToLowerInvariant();
}
modelBuilder.MakeDbObjectNamesLowercase();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using Microsoft.EntityFrameworkCore;

namespace EdFi.Admin.DataAccess.Contexts
{
public class SqlServerUsersContext : UsersContext
{
public SqlServerUsersContext(string connectionString) : base(connectionString) { }
public SqlServerUsersContext(DbContextOptions options) : base(options) { }
}
}
76 changes: 45 additions & 31 deletions Application/EdFi.Admin.DataAccess/Contexts/UsersContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,22 @@

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using EdFi.Admin.DataAccess.Extensions;
using EdFi.Admin.DataAccess.Models;
using EdFi.Admin.DataAccess.Utils;
using Microsoft.EntityFrameworkCore;

namespace EdFi.Admin.DataAccess.Contexts
{
public abstract class UsersContext : DbContext, IUsersContext
{

protected UsersContext(string connectionString)
: base(connectionString)
{
Database.SetInitializer(new ValidateDatabase<SqlServerUsersContext>());
Database.SetInitializer(new ValidateDatabase<PostgresUsersContext>());
}
protected UsersContext(DbContextOptions options)
: base(options) { }

public const string UserTableName = "Users";

public static string UserNameColumn
Expand All @@ -34,55 +33,70 @@ public static string UserIdColumn
get { return UserMemberName(x => x.UserId); }
}

public IDbSet<User> Users { get; set; }
public DbSet<User> Users { get; set; }

public IDbSet<ApiClient> Clients { get; set; }
public DbSet<ApiClient> Clients { get; set; }

public IDbSet<ClientAccessToken> ClientAccessTokens { get; set; }
public DbSet<ClientAccessToken> ClientAccessTokens { get; set; }

public IDbSet<Vendor> Vendors { get; set; }
public DbSet<Vendor> Vendors { get; set; }

public IDbSet<Application> Applications { get; set; }
public DbSet<Application> Applications { get; set; }

public IDbSet<Profile> Profiles { get; set; }
public DbSet<Profile> Profiles { get; set; }

public IDbSet<OdsInstance> OdsInstances { get; set; }
public DbSet<OdsInstance> OdsInstances { get; set; }

public IDbSet<OdsInstanceComponent> OdsInstanceComponents { get; set; }
public DbSet<OdsInstanceComponent> OdsInstanceComponents { get; set; }

//TODO: This should really be removed from being directly on the context. Application should own
//TODO: these instances, and deleting an application should delete the associated LEA's
public IDbSet<ApplicationEducationOrganization> ApplicationEducationOrganizations { get; set; }
public DbSet<ApplicationEducationOrganization> ApplicationEducationOrganizations { get; set; }

public IDbSet<VendorNamespacePrefix> VendorNamespacePrefixes { get; set; }
public DbSet<VendorNamespacePrefix> VendorNamespacePrefixes { get; set; }

public IDbSet<OwnershipToken> OwnershipToken { get; set; }
public DbSet<OwnershipToken> OwnershipTokens { get; set; }

public IDbSet<WebPagesUsersInRoles> UsersInRoles { get; set; }
public DbSet<ApiClientOwnershipToken> ApiClientOwnershipTokens { get; set; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
public DbSet<WebPagesUsersInRoles> UsersInRoles { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ApplyProviderSpecificMappings(modelBuilder);
modelBuilder.Entity<ApiClient>()
.HasMany(t => t.ApplicationEducationOrganizations)
.WithMany(t => t.Clients)
.UsingEntity<ApiClientApplicationEducationOrganization>(
"ApiClientApplicationEducationOrganizations",
l =>
l.HasOne<ApplicationEducationOrganization>().WithMany().HasForeignKey(
"ApplicationEducationOrganizationId"),
r =>
r.HasOne<ApiClient>().WithMany().HasForeignKey("ApiClientId"));

modelBuilder.Entity<Application>()
.HasMany(a => a.Profiles)
.WithMany(a => a.Applications)
.UsingEntity("ProfileApplications");

modelBuilder.UseUnderscoredFkColumnNames();

modelBuilder.Model.FindEntityTypes(typeof(ApiClient)).First().GetProperty("CreatorOwnershipTokenId")
.SetColumnName("CreatorOwnershipTokenId_OwnershipTokenId");
}

/// <remarks>
/// Sub-classes should override this to provide database system-specific column and/or
/// table mappings: for example, if a linking table column in Postgres needs to map to a
/// name other than the default provided by Entity Framework.
/// </remarks>
protected virtual void ApplyProviderSpecificMappings(DbModelBuilder modelBuilder) { }

/// <inheritdoc />
public Task<int> ExecuteSqlCommandAsync(string sqlStatement, params object[] parameters)
{
return Database.ExecuteSqlCommandAsync(sqlStatement.ToLowerInvariant(), parameters);
return Database.ExecuteSqlInterpolatedAsync(
FormattableStringFactory.Create(sqlStatement.ToLowerInvariant(), parameters));
}

/// <inheritdoc />
public async Task<IReadOnlyList<TReturn>> ExecuteQueryAsync<TReturn>(string sqlStatement, params object[] parameters)
{
return await Database
.SqlQuery<TReturn>(sqlStatement.ToLowerInvariant(), parameters)
.SqlQueryRaw<TReturn>(sqlStatement.ToLowerInvariant(), parameters)
.ToListAsync();
}

Expand Down
34 changes: 31 additions & 3 deletions Application/EdFi.Admin.DataAccess/Contexts/UsersContextFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
using EdFi.Admin.DataAccess.Providers;
using EdFi.Common;
using EdFi.Common.Configuration;
using Microsoft.EntityFrameworkCore;

namespace EdFi.Admin.DataAccess.Contexts
{
public class UsersContextFactory : IUsersContextFactory
{
private readonly Dictionary<DatabaseEngine, Type> _usersContextTypeByDatabaseEngine = new Dictionary<DatabaseEngine, Type>
private readonly Dictionary<DatabaseEngine, Type> _usersContextTypeByDatabaseEngine = new()
{
{DatabaseEngine.SqlServer, typeof(SqlServerUsersContext)},
{DatabaseEngine.Postgres, typeof(PostgresUsersContext)}
Expand All @@ -28,12 +29,39 @@ public UsersContextFactory(IAdminDatabaseConnectionStringProvider connectionStri
_connectionStringsProvider = Preconditions.ThrowIfNull(connectionStringsProvider, nameof(connectionStringsProvider));
_databaseEngine = Preconditions.ThrowIfNull(databaseEngine, nameof(databaseEngine));
}
public Type GetUsersContextType()
{
if (_usersContextTypeByDatabaseEngine.TryGetValue(_databaseEngine, out Type contextType))
{
return contextType;
}

throw new InvalidOperationException(
$"No UsersContext defined for database type {_databaseEngine.DisplayName}");
}

public IUsersContext CreateContext()
{
if (_usersContextTypeByDatabaseEngine.TryGetValue(_databaseEngine, out Type contextType))
if (_databaseEngine == DatabaseEngine.SqlServer)
{
return Activator.CreateInstance(
GetUsersContextType(),
new DbContextOptionsBuilder<SqlServerUsersContext>()
.UseLazyLoadingProxies()
.UseSqlServer(_connectionStringsProvider.GetConnectionString())
.Options) as
IUsersContext;
}

if (_databaseEngine == DatabaseEngine.Postgres)
{
return Activator.CreateInstance(contextType, _connectionStringsProvider.GetConnectionString()) as IUsersContext;
return Activator.CreateInstance(
GetUsersContextType(),
new DbContextOptionsBuilder<PostgresUsersContext>()
.UseLazyLoadingProxies()
.UseNpgsql(_connectionStringsProvider.GetConnectionString())
.Options) as
IUsersContext;
}

throw new InvalidOperationException(
Expand Down
15 changes: 9 additions & 6 deletions Application/EdFi.Admin.DataAccess/EdFi.Admin.DataAccess.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>EdFi.Admin.DataAccess</PackageId>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<AssemblyName>EdFi.Admin.DataAccess</AssemblyName>
<RootNamespace>EdFi.Admin.DataAccess</RootNamespace>
Expand All @@ -17,12 +17,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EdFi.Suite3.Common" Version="5.4.447" />
<PackageReference Include="EntityFramework" Version="6.4.4" />
<PackageReference Include="EntityFramework6.Npgsql" Version="6.4.3" />
<PackageReference Include="log4net" Version="2.0.13" />
<PackageReference Include="Npgsql" Version="6.0.11" />
<PackageReference Include="log4net" Version="2.0.17" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
</ItemGroup>
<ItemGroup>
<None Include="../../LICENSE.txt" Pack="true" PackagePath="LICENSE.txt" />
</ItemGroup>
</Project>
</Project>
Loading
Loading