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

Repository Management #3927

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Diagnostics.Tracing;
using DevHome.Telemetry;
using Microsoft.Diagnostics.Telemetry.Internal;

namespace DevHome.Common.TelemetryEvents.DevHomeDatabase;

[EventData]
public class DatabaseMigrationErrorEvent : EventBase
{
public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServicePerformance;

public uint PreviousSchemaVersion { get; set; }

public uint CurrentSchemaVersion { get; set; }

public int HResult { get; }

public string ExceptionMessage { get; } = string.Empty;

public DatabaseMigrationErrorEvent(Exception ex, uint previousSchemaVersion, uint currentSchemaVersion)
{
HResult = ex.HResult;
ExceptionMessage = ex.Message;
PreviousSchemaVersion = previousSchemaVersion;
CurrentSchemaVersion = currentSchemaVersion;
}

public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
{
// No sensitive strings to replace.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@ public class RepositoryLineItemErrorEvent : EventBase

public string ErrorMessage { get; } = string.Empty;

public string RepositoryName { get; } = string.Empty;

public RepositoryLineItemErrorEvent(string action, int hresult, string errorMessage, string repositoryName)
public RepositoryLineItemErrorEvent(string action, Exception ex)
{
Action = action;
Hresult = hresult;
ErrorMessage = errorMessage;
RepositoryName = repositoryName;
Hresult = ex.HResult;
ErrorMessage = ex.Message;
}

public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ public class RepositoryLineItemEvent : EventBase

public string Action { get; } = string.Empty;

public string RepositoryName { get; } = string.Empty;

public RepositoryLineItemEvent(string action, string repositoryName)
public RepositoryLineItemEvent(string action)
{
Action = action;
RepositoryName = repositoryName;
}

public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
Expand Down
26 changes: 26 additions & 0 deletions database/DevHome.Database/Assets/MigrationScripts/0To1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);

BEGIN TRANSACTION;

CREATE TABLE "Repository" (
"RepositoryId" INTEGER NOT NULL CONSTRAINT "PK_Repository" PRIMARY KEY AUTOINCREMENT,
"RepositoryName" TEXT NOT NULL DEFAULT '',
"RepositoryClonePath" TEXT NOT NULL DEFAULT '',
"IsHidden" INTEGER NOT NULL,
"ConfigurationFileLocation" TEXT NULL DEFAULT '',
"RepositoryUri" TEXT NULL DEFAULT '',
"SourceControlClassId" TEXT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
"CreatedUTCDate" TEXT NULL DEFAULT (datetime()),
"UpdatedUTCDate" TEXT NULL DEFAULT (datetime())
);

CREATE UNIQUE INDEX "IX_Repository_RepositoryName_RepositoryClonePath" ON "Repository" ("RepositoryName", "RepositoryClonePath");

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20240920200626_InitialMigration', '8.0.8');

COMMIT;

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using DevHome.Common.TelemetryEvents.DevHomeDatabase;
using DevHome.Database.DatabaseModels.RepositoryManagement;
using DevHome.Telemetry;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Serilog;

namespace DevHome.Database.Configurations;

public class RepositoryConfiguration : IEntityTypeConfiguration<Repository>
{
private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(RepositoryConfiguration));

public void Configure(EntityTypeBuilder<Repository> builder)
{
try
{
builder.Property(x => x.ConfigurationFileLocation).HasDefaultValue(string.Empty);
builder.Property(x => x.RepositoryClonePath).HasDefaultValue(string.Empty).IsRequired(true);
builder.Property(x => x.RepositoryName).HasDefaultValue(string.Empty).IsRequired(true);
builder.Property(x => x.CreatedUTCDate).HasDefaultValueSql("datetime()");
builder.Property(x => x.UpdatedUTCDate).HasDefaultValueSql("datetime()");
builder.Property(x => x.RepositoryUri).HasDefaultValue(string.Empty);
builder.ToTable("Repository");
}
catch (Exception ex)
{
_log.Error(ex, "Error building the repository data model.");
TelemetryFactory.Get<ITelemetry>().Log(
"DevHome_RepositoryConfiguration_Event",
LogLevel.Critical,
new DatabaseContextErrorEvent("CreatingRepositoryModel", ex));
}
}
}
5 changes: 5 additions & 0 deletions database/DevHome.Database/DevHome.Database.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
<!-- This tells the build to copy dependent libraries to the output directory.-->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\MigrationScripts\0To1.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
Expand Down
68 changes: 38 additions & 30 deletions database/DevHome.Database/DevHomeDatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,29 @@

using System;
using System.IO;
using System.Linq.Expressions;
using DevHome.Common.Helpers;
using DevHome.Common.TelemetryEvents.DevHomeDatabase;
using DevHome.Database.Configurations;
using DevHome.Database.DatabaseModels.RepositoryManagement;
using DevHome.Database.Services;
using DevHome.Telemetry;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Serilog;
using Windows.Storage;

namespace DevHome.Database;

// TODO: Add documentation around migration and Entity Framework in DevHome.

/// <summary>
/// To make the database please run the following in Package Manager Console
/// Update-Database -StartupProject DevHome.Database -Project DevHome.Database
///
/// TODO: Remove this comment after database migration is implemeneted.
/// TODO: Set up Github detection for files in this project.
/// TODO: Add documentation around migration and Entity Framework in DevHome.
/// Provides access to the database for DevHome.
/// </summary>
public class DevHomeDatabaseContext : DbContext
public class DevHomeDatabaseContext : DbContext, IDevHomeDatabaseContext
{
private const string DatabaseFileName = "DevHome.db";
// Increment when the schema has changed.
// Should incremenet once per release.
public uint SchemaVersion => 1;

private readonly ILogger _log = Log.ForContext("SourceContext", nameof(DevHomeDatabaseContext));

Expand All @@ -33,35 +35,36 @@ public class DevHomeDatabaseContext : DbContext

public DevHomeDatabaseContext()
{
// TODO: How to run the DevHome in VS and not have the file move to the per app location.
DbPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), DatabaseFileName);
if (RuntimeHelper.IsMSIX)
{
DbPath = Path.Join(ApplicationData.Current.LocalFolder.Path, "DevHome.db");
}
else
{
#if CANARY_BUILD
var databaseFileName = "DevHome_Canary.db";
#elif STABLE_BUILD
var databaseFileName = "DevHome.db";
#else
var databaseFileName = "DevHome_dev.db";
#endif
DbPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), databaseFileName);
}
}

public DevHomeDatabaseContext(string dbPath)
{
DbPath = dbPath;
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// TODO: Use ServiceExtensions as an example to set up individual
// models using fluent API. Currently, not needed, but will as this method
// will expand as more entities are added.
// If that is too much work these definitions can be placed inside the C# class.
try
{
// TODO: How to update "UpdatedAt"?
var repositoryEntity = modelBuilder.Entity<Repository>();
if (repositoryEntity != null)
{
repositoryEntity.Property(x => x.ConfigurationFileLocation).HasDefaultValue(string.Empty);
repositoryEntity.Property(x => x.RepositoryClonePath).HasDefaultValue(string.Empty).IsRequired(true);
repositoryEntity.Property(x => x.RepositoryName).HasDefaultValue(string.Empty).IsRequired(true);
repositoryEntity.Property(x => x.CreatedUTCDate).HasDefaultValueSql("datetime()");
repositoryEntity.Property(x => x.UpdatedUTCDate).HasDefaultValueSql("datetime()");
repositoryEntity.Property(x => x.RepositoryUri).HasDefaultValue(string.Empty);
repositoryEntity.Property(x => x.SourceControlClassId).HasDefaultValue(Guid.Empty);
repositoryEntity.ToTable("Repository");
}
new RepositoryConfiguration().Configure(modelBuilder.Entity<Repository>());
}
catch (Exception ex)
{
// TODO: Notify user the database could not initialize.
_log.Error(ex, "Can not build the database model");
TelemetryFactory.Get<ITelemetry>().Log(
"DevHome_DatabaseContext_Event",
Expand All @@ -74,4 +77,9 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite($"Data Source={DbPath}");
}

public EntityEntry Add(Repository repository)
{
return Repositories.Add(repository);
}
}
8 changes: 7 additions & 1 deletion database/DevHome.Database/Extensions/ServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ public static class ServiceExtensions
{
public static IServiceCollection AddDatabase(this IServiceCollection services, HostBuilderContext context)
{
services.AddSingleton<DevHomeDatabaseContextFactory>();
services.AddSingleton<IDevHomeDatabaseContextFactory, DevHomeDatabaseContextFactory>();

services.AddSingleton<RepositoryManagementDataAccessService>();

services.AddSingleton<ISchemaAccessFactory, SchemaAccessorFactory>();

services.AddSingleton<ICustomMigrationHandlerFactory, CustomMigrationHandlerFactory>();

services.AddSingleton<DatabaseMigrationService>();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using DevHome.Database.Services;

namespace DevHome.Database.Factories;

public class CustomMigrationHandlerFactory : ICustomMigrationHandlerFactory
{
/// <summary>
/// Gets a list of all migrations that can handle the migration from previousSchemaVersion to
/// currentSchemaVersion in priority order (min to max).
/// </summary>
/// <param name="previousSchemaVersion">The schema version upgrading from.</param>
/// <param name="currentSchemaVersion">The schema version upgrading to.</param>
/// <remarks>Specifically, this grabs all classes the inherit from ICustomMigration that is not
/// abstract, not generic, and has a parameterless constructor.</remarks>
/// <returns>A list of all objects that can handle the migration, sorted in priority order.</returns>
public IReadOnlyList<ICustomMigration> GetCustomMigrationHandlers(uint previousSchemaVersion, uint currentSchemaVersion)
{
var customLogics = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => typeof(ICustomMigration).IsAssignableFrom(type))
.Where(type =>
!type.IsAbstract &&
!type.IsGenericType &&
type.GetConstructor(Array.Empty<Type>()) != null)
.Select(type => (ICustomMigration)Activator.CreateInstance(type)!)
.OrderBy(x => x.Priority)
.ToList();
return customLogics;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using DevHome.Database.Services;
using Serilog;

namespace DevHome.Database.Factories;

public class DevHomeDatabaseContextFactory
public class DevHomeDatabaseContextFactory : IDevHomeDatabaseContextFactory
{
private readonly ILogger _log = Log.ForContext("SourceContext", nameof(DevHomeDatabaseContextFactory));

public DevHomeDatabaseContext GetNewContext()
public IDevHomeDatabaseContext GetNewContext()
{
_log.Information("Making a new DevHome Database Context");

Expand Down
25 changes: 25 additions & 0 deletions database/DevHome.Database/Factories/SchemaAccessorFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using DevHome.Common.Helpers;
using DevHome.Database.Services;

namespace DevHome.Database.Factories;

/// <summary>
/// Accessing the schema file differs between MSIX and winexe.
/// </summary>
public class SchemaAccessorFactory : ISchemaAccessFactory
{
public ISchemaAccessor GenerateSchemaAccessor()
{
if (RuntimeHelper.IsMSIX)
{
return new MSIXSchemaAccessor();
}
else
{
return new WinExeSchemaAccessor();
}
}
}
9 changes: 9 additions & 0 deletions database/DevHome.Database/SchemaAccessorConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace DevHome.Database;

internal static class SchemaAccessorConstants
{
internal const string SchemaVersionFileName = "SchemaVersion.txt";
}
Loading
Loading