Skip to content
Merged
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
Expand Up @@ -5,7 +5,6 @@

using System;
using System.IO;
using EntityFrameworkCore.Scaffolding.Handlebars.Helpers;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

// Modifications copyright(C) 2018 Tony Sneed.

using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
using Microsoft.EntityFrameworkCore.Storage.Internal;

namespace EntityFrameworkCore.Scaffolding.Handlebars
{
/// <summary>
/// Scaffolding for persisting generated DbContext and entity type classes using Handlebars templates.
/// </summary>
public class HbsReverseEngineerScaffolder : ReverseEngineerScaffolder
{
/// <summary>
/// Constructor for the HbsCSharpModelGenerator.
/// </summary>
/// <param name="databaseModelFactory">Service to reverse engineer a database into a database model.</param>
/// <param name="scaffoldingModelFactory">Factory to create a scaffolding model.</param>
/// <param name="modelCodeGeneratorSelector">Selects a model code generator service for a given programming language.</param>
/// <param name="cSharpUtilities">C# utilities.</param>
/// <param name="cSharpHelper">C# helper.</param>
/// <param name="connectionStringResolver">Connection string resolver.</param>
public HbsReverseEngineerScaffolder(
IDatabaseModelFactory databaseModelFactory,
IScaffoldingModelFactory scaffoldingModelFactory,
IModelCodeGeneratorSelector modelCodeGeneratorSelector,
ICSharpUtilities cSharpUtilities,
ICSharpHelper cSharpHelper,
INamedConnectionStringResolver connectionStringResolver) : base(databaseModelFactory, scaffoldingModelFactory, modelCodeGeneratorSelector, cSharpUtilities, cSharpHelper, connectionStringResolver)
{
}

/// <summary>
/// Persist generated DbContext and entity type classes.
/// </summary>
/// <param name="scaffoldedModel">Represents a scaffolded model.</param>
/// <param name="outputDir">Output directory.</param>
/// <param name="overwriteFiles">True to overwrite existing files.</param>
/// <returns></returns>
public override SavedModelFiles Save(ScaffoldedModel scaffoldedModel, string outputDir, bool overwriteFiles)
{
CheckOutputFiles(scaffoldedModel, outputDir, overwriteFiles);

Directory.CreateDirectory(outputDir);

var contextPath = string.Empty;
if (scaffoldedModel.ContextFile != null
&& !string.IsNullOrWhiteSpace(scaffoldedModel.ContextFile.Path))
{
contextPath = Path.GetFullPath(Path.Combine(outputDir, scaffoldedModel.ContextFile.Path));
Directory.CreateDirectory(Path.GetDirectoryName(contextPath));
File.WriteAllText(contextPath, scaffoldedModel.ContextFile.Code, Encoding.UTF8);
}

var additionalFiles = new List<string>();
foreach (var entityTypeFile in scaffoldedModel.AdditionalFiles)
{
var additionalFilePath = Path.Combine(outputDir, entityTypeFile.Path);
File.WriteAllText(additionalFilePath, entityTypeFile.Code, Encoding.UTF8);
additionalFiles.Add(additionalFilePath);
}

return new SavedModelFiles(contextPath, additionalFiles);
}

private static void CheckOutputFiles(
ScaffoldedModel scaffoldedModel,
string outputDir,
bool overwriteFiles)
{
var paths = scaffoldedModel.AdditionalFiles.Select(f => f.Path).ToList();
if (scaffoldedModel.ContextFile != null
&& !string.IsNullOrWhiteSpace(scaffoldedModel.ContextFile.Path))
paths.Insert(0, scaffoldedModel.ContextFile.Path);

var existingFiles = new List<string>();
var readOnlyFiles = new List<string>();
foreach (var path in paths)
{
var fullPath = Path.Combine(outputDir, path);

if (File.Exists(fullPath))
{
existingFiles.Add(path);

if (File.GetAttributes(fullPath).HasFlag(FileAttributes.ReadOnly))
{
readOnlyFiles.Add(path);
}
}
}

if (!overwriteFiles && existingFiles.Count != 0)
{
throw new OperationException(
DesignStrings.ExistingFiles(
outputDir,
string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator, existingFiles)));
}
if (readOnlyFiles.Count != 0)
{
throw new OperationException(
DesignStrings.ReadOnlyFiles(
outputDir,
string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator, readOnlyFiles)));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public static IServiceCollection AddHandlebarsScaffolding(this IServiceCollectio
return new HbsHelperService(helpers);
});
services.AddSingleton<IModelCodeGenerator, HbsCSharpModelGenerator>();
services.AddSingleton<IReverseEngineerScaffolder, HbsReverseEngineerScaffolder>();
return services;
}
}
Expand Down
200 changes: 184 additions & 16 deletions test/Scaffolding.Handlebars.Tests/HbsCSharpScaffoldingGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,24 @@ public HbsCSharpScaffoldingGeneratorTests(NorthwindDbContextFixture fixture)
[InlineData(true)]
public void WriteCode_Should_Generate_Context_File(bool useDataAnnotations)
{
// Arrange
var options = ReverseEngineerOptions.DbContextOnly;
var scaffolder = CreateScaffolder(options);

// Act
var model = scaffolder.ScaffoldModel(
connectionString: Constants.Connections.SqlServerConnection,
tables: Enumerable.Empty<string>(),
schemas: Enumerable.Empty<string>(),
@namespace: Constants.Parameters.RootNamespace,
language: "C#",
contextName: Constants.Parameters.ContextName,
modelOptions: new ModelReverseEngineerOptions(),
contextDir: Constants.Parameters.ProjectPath,
codeOptions: new ModelCodeGenerationOptions { UseDataAnnotations = useDataAnnotations });

// Act
Dictionary<string, string> files = ReverseEngineerFiles(ReverseEngineerOptions.DbContextOnly, useDataAnnotations);
var files = GetGeneratedFiles(model, options);

// Assert
object expectedContext = useDataAnnotations
Expand All @@ -117,8 +133,24 @@ public void WriteCode_Should_Generate_Context_File(bool useDataAnnotations)
[InlineData(true)]
public void WriteCode_Should_Generate_Entity_Files(bool useDataAnnotations)
{
// Arrange
var options = ReverseEngineerOptions.EntitiesOnly;
var scaffolder = CreateScaffolder(options);

// Act
var model = scaffolder.ScaffoldModel(
connectionString: Constants.Connections.SqlServerConnection,
tables: Enumerable.Empty<string>(),
schemas: Enumerable.Empty<string>(),
@namespace: Constants.Parameters.RootNamespace,
language: "C#",
contextName: Constants.Parameters.ContextName,
modelOptions: new ModelReverseEngineerOptions(),
contextDir: Constants.Parameters.ProjectPath,
codeOptions: new ModelCodeGenerationOptions { UseDataAnnotations = useDataAnnotations });

// Act
Dictionary<string, string> files = ReverseEngineerFiles(ReverseEngineerOptions.EntitiesOnly, useDataAnnotations);
var files = GetGeneratedFiles(model, options);

// Assert
var category = files[Constants.Files.CategoryFile];
Expand All @@ -135,7 +167,150 @@ public void WriteCode_Should_Generate_Entity_Files(bool useDataAnnotations)
Assert.Equal(expectedProduct, product);
}

private Dictionary<string, string> ReverseEngineerFiles(ReverseEngineerOptions options, bool useDataAnnotations)
[Theory]
[InlineData(false)]
[InlineData(true)]
public void WriteCode_Should_Generate_Context_and_Entity_Files(bool useDataAnnotations)
{
// Arrange
var options = ReverseEngineerOptions.DbContextAndEntities;
var scaffolder = CreateScaffolder(options);

// Act
var model = scaffolder.ScaffoldModel(
connectionString: Constants.Connections.SqlServerConnection,
tables: Enumerable.Empty<string>(),
schemas: Enumerable.Empty<string>(),
@namespace: Constants.Parameters.RootNamespace,
language: "C#",
contextName: Constants.Parameters.ContextName,
modelOptions: new ModelReverseEngineerOptions(),
contextDir: Constants.Parameters.ProjectPath,
codeOptions: new ModelCodeGenerationOptions { UseDataAnnotations = useDataAnnotations });

// Act
Dictionary<string, string> files = GetGeneratedFiles(model, options);

// Assert
object expectedContext = useDataAnnotations
? ExpectedContextsWithAnnotations.ContextClass
: ExpectedContexts.ContextClass;
object expectedCategory = useDataAnnotations
? ExpectedEntitiesWithAnnotations.CategoryClass
: ExpectedEntities.CategoryClass;
object expectedProduct = useDataAnnotations
? ExpectedEntitiesWithAnnotations.ProductClass
: ExpectedEntities.ProductClass;

var context = files[Constants.Files.DbContextFile];
var category = files[Constants.Files.CategoryFile];
var product = files[Constants.Files.ProductFile];

Assert.Equal(expectedContext, context);
Assert.Equal(expectedCategory, category);
Assert.Equal(expectedProduct, product);
}

[Fact]
public void Save_Should_Write_Context_File()
{
using (var directory = new TempDirectory())
{
// Arrange
var scaffolder = CreateScaffolder(ReverseEngineerOptions.DbContextOnly);
var model = scaffolder.ScaffoldModel(
connectionString: Constants.Connections.SqlServerConnection,
tables: Enumerable.Empty<string>(),
schemas: Enumerable.Empty<string>(),
@namespace: Constants.Parameters.RootNamespace,
language: "C#",
contextName: Constants.Parameters.ContextName,
modelOptions: new ModelReverseEngineerOptions(),
contextDir: Path.Combine(directory.Path, "Contexts"),
codeOptions: new ModelCodeGenerationOptions { UseDataAnnotations = false });

// Act
var result = scaffolder.Save(model,
Path.Combine(directory.Path, "Models"),
overwriteFiles: false);

// Assert
var expectedContextPath = Path.Combine(directory.Path, "Contexts", Constants.Files.DbContextFile);
var expectedCategoryPath = Path.Combine(directory.Path, "Models", Constants.Files.CategoryFile);
var expectedProductPath = Path.Combine(directory.Path, "Models", Constants.Files.ProductFile);
Assert.Equal(expectedContextPath, result.ContextFile);
Assert.False(File.Exists(expectedCategoryPath));
Assert.False(File.Exists(expectedProductPath));
}
}

[Fact]
public void Save_Should_Write_Entity_Files()
{
using (var directory = new TempDirectory())
{
// Arrange
var scaffolder = CreateScaffolder(ReverseEngineerOptions.EntitiesOnly);
var model = scaffolder.ScaffoldModel(
connectionString: Constants.Connections.SqlServerConnection,
tables: Enumerable.Empty<string>(),
schemas: Enumerable.Empty<string>(),
@namespace: Constants.Parameters.RootNamespace,
language: "C#",
contextName: Constants.Parameters.ContextName,
modelOptions: new ModelReverseEngineerOptions(),
contextDir: Path.Combine(directory.Path, "Contexts"),
codeOptions: new ModelCodeGenerationOptions { UseDataAnnotations = false });

// Act
var result = scaffolder.Save(model,
Path.Combine(directory.Path, "Models"),
overwriteFiles: false);

// Assert
var expectedContextPath = Path.Combine(directory.Path, "Contexts", Constants.Files.DbContextFile);
var expectedCategoryPath = Path.Combine(directory.Path, "Models", Constants.Files.CategoryFile);
var expectedProductPath = Path.Combine(directory.Path, "Models", Constants.Files.ProductFile);
Assert.Equal(expectedCategoryPath, result.AdditionalFiles[0]);
Assert.Equal(expectedProductPath, result.AdditionalFiles[1]);
Assert.False(File.Exists(expectedContextPath));
}
}

[Fact]
public void Save_Should_Write_Context_and_Entity_Files()
{
using (var directory = new TempDirectory())
{
// Arrange
var scaffolder = CreateScaffolder(ReverseEngineerOptions.DbContextAndEntities);
var model = scaffolder.ScaffoldModel(
connectionString: Constants.Connections.SqlServerConnection,
tables: Enumerable.Empty<string>(),
schemas: Enumerable.Empty<string>(),
@namespace: Constants.Parameters.RootNamespace,
language: "C#",
contextName: Constants.Parameters.ContextName,
modelOptions: new ModelReverseEngineerOptions(),
contextDir: Path.Combine(directory.Path, "Contexts"),
codeOptions: new ModelCodeGenerationOptions { UseDataAnnotations = false });

// Act
var result = scaffolder.Save(model,
Path.Combine(directory.Path, "Models"),
overwriteFiles: false);

// Assert
var expectedContextPath = Path.Combine(directory.Path, "Contexts", Constants.Files.DbContextFile);
var expectedCategoryPath = Path.Combine(directory.Path, "Models", Constants.Files.CategoryFile);
var expectedProductPath = Path.Combine(directory.Path, "Models", Constants.Files.ProductFile);
Assert.Equal(expectedContextPath, result.ContextFile);
Assert.Equal(expectedCategoryPath, result.AdditionalFiles[0]);
Assert.Equal(expectedProductPath, result.AdditionalFiles[1]);
}
}

private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions options)
{
var fileService = new InMemoryTemplateFileService();
fileService.InputFiles(ContextClassTemplate, ContextImportsTemplate, ContextDbSetsTemplate,
Expand Down Expand Up @@ -176,25 +351,18 @@ private Dictionary<string, string> ReverseEngineerFiles(ReverseEngineerOptions o
new HbsHelperService(new Dictionary<string, Action<TextWriter, object, object[]>>
{
{EntityFrameworkCore.Scaffolding.Handlebars.Helpers.Constants.SpacesHelper, HandlebarsHelpers.SpacesHelper}
}));
}))
.AddSingleton<IReverseEngineerScaffolder, HbsReverseEngineerScaffolder>();

new SqlServerDesignTimeServices().ConfigureDesignTimeServices(services);
var scaffolder = services
.BuildServiceProvider()
.GetRequiredService<IReverseEngineerScaffolder>();
return scaffolder;
}

// Act
var model = scaffolder.ScaffoldModel(
connectionString: Constants.Connections.SqlServerConnection,
tables: Enumerable.Empty<string>(),
schemas: Enumerable.Empty<string>(),
@namespace: Constants.Parameters.RootNamespace,
language: "C#",
contextName: Constants.Parameters.ContextName,
modelOptions: new ModelReverseEngineerOptions(),
contextDir: Constants.Parameters.ProjectPath,
codeOptions: new ModelCodeGenerationOptions { UseDataAnnotations = useDataAnnotations });

private Dictionary<string, string> GetGeneratedFiles(ScaffoldedModel model, ReverseEngineerOptions options)
{
var generatedFiles = new Dictionary<string, string>();

if (options == ReverseEngineerOptions.DbContextOnly
Expand Down
20 changes: 20 additions & 0 deletions test/Scaffolding.Handlebars.Tests/Helpers/TempDirectory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.IO;
using IOPath = System.IO.Path;

namespace Scaffolding.Handlebars.Tests.Helpers
{
public class TempDirectory : IDisposable
{
public TempDirectory()
{
Path = IOPath.Combine(IOPath.GetTempPath(), IOPath.GetRandomFileName());
Directory.CreateDirectory(Path);
}

public string Path { get; }

public void Dispose()
=> Directory.Delete(Path, recursive: true);
}
}
Loading