diff --git a/PowerTools.sln b/PowerTools.sln new file mode 100644 index 00000000..71fd2bcf --- /dev/null +++ b/PowerTools.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 11 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{2AE1B177-580C-44F1-9514-B3F23F4B1433}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerTools", "src\PowerTools\PowerTools.csproj", "{16CAD3A8-FCE0-4BC1-901A-16957CF24BD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerTools.Test", "test\PowerTools.Test\PowerTools.Test.csproj", "{ADCD3A3D-2564-48F3-81A9-B0B532675808}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {16CAD3A8-FCE0-4BC1-901A-16957CF24BD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16CAD3A8-FCE0-4BC1-901A-16957CF24BD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16CAD3A8-FCE0-4BC1-901A-16957CF24BD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16CAD3A8-FCE0-4BC1-901A-16957CF24BD6}.Release|Any CPU.Build.0 = Release|Any CPU + {ADCD3A3D-2564-48F3-81A9-B0B532675808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADCD3A3D-2564-48F3-81A9-B0B532675808}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADCD3A3D-2564-48F3-81A9-B0B532675808}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADCD3A3D-2564-48F3-81A9-B0B532675808}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {ADCD3A3D-2564-48F3-81A9-B0B532675808} = {2AE1B177-580C-44F1-9514-B3F23F4B1433} + EndGlobalSection +EndGlobal diff --git a/packages/repositories.config b/packages/repositories.config index a77aff03..fa9b2e99 100644 --- a/packages/repositories.config +++ b/packages/repositories.config @@ -3,4 +3,5 @@ + \ No newline at end of file diff --git a/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Context.tt b/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Context.tt new file mode 100644 index 00000000..389ed51f --- /dev/null +++ b/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Context.tt @@ -0,0 +1,47 @@ +<#@ template hostspecific="true" language="C#" #> +<#@ include file="EF.Utility.CS.ttinclude" #><#@ + output extension=".cs" #><# + + var efHost = (EfTextTemplateHost)Host; + var code = new CodeGenerationTools(this); +#> +using System.Data.Entity; +using System.Data.Entity.Infrastructure; +using <#= code.EscapeNamespace(efHost.MappingNamespace) #>; + +namespace <#= code.EscapeNamespace(efHost.Namespace) #> +{ + public class <#= efHost.EntityContainer.Name #> : DbContext + { + static <#= efHost.EntityContainer.Name #>() + { + Database.SetInitializer<<#= efHost.EntityContainer.Name #>>(null); + } + + public <#= efHost.EntityContainer.Name #>() + : base("Name=<#= efHost.EntityContainer.Name #>") + { + } + +<# + foreach (var set in efHost.EntityContainer.BaseEntitySets.OfType()) + { +#> + public DbSet<<#= set.ElementType.Name #>> <#= set.Name #> { get; set; } +<# + } +#> + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { +<# + foreach (var set in efHost.EntityContainer.BaseEntitySets.OfType()) + { +#> + modelBuilder.Configurations.Add(new <#= set.ElementType.Name #>Map()); +<# + } +#> + } + } +} diff --git a/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Entity.tt b/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Entity.tt new file mode 100644 index 00000000..6588c736 --- /dev/null +++ b/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Entity.tt @@ -0,0 +1,63 @@ +<#@ template hostspecific="true" language="C#" #> +<#@ include file="EF.Utility.CS.ttinclude" #><#@ + output extension=".cs" #><# + + var efHost = (EfTextTemplateHost)Host; + var code = new CodeGenerationTools(this); +#> +using System; +using System.Collections.Generic; + +namespace <#= code.EscapeNamespace(efHost.Namespace) #> +{ + public class <#= efHost.EntityType.Name #> + { +<# + var collectionNavigations = efHost.EntityType.NavigationProperties.Where( + np => np.DeclaringType == efHost.EntityType + && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many); + + // Add a ctor to initialize any collections + if (collectionNavigations.Any()) + { +#> + public <#= code.Escape(efHost.EntityType) #>() + { +<# + foreach (var navProperty in collectionNavigations) + { +#> + this.<#= code.Escape(navProperty) #> = new List<<#= code.Escape(navProperty.ToEndMember.GetEntityType()) #>>(); +<# + } +#> + } + +<# + } + + foreach (var property in efHost.EntityType.Properties) + { +#> + <#= Accessibility.ForProperty(property) #> <#= code.Escape(property.TypeUsage) #> <#= code.Escape(property) #> { get; set; } +<# + } + + foreach (var navProperty in efHost.EntityType.NavigationProperties.Where(np => np.DeclaringType == efHost.EntityType)) + { + if (navProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) + { +#> + public virtual ICollection<<#= code.Escape(navProperty.ToEndMember.GetEntityType()) #>> <#= code.Escape(navProperty) #> { get; set; } +<# + } + else + { +#> + public virtual <#= code.Escape(navProperty.ToEndMember.GetEntityType()) #> <#= code.Escape(navProperty) #> { get; set; } +<# + } + } +#> + } +} diff --git a/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Mapping.tt b/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Mapping.tt new file mode 100644 index 00000000..30c427a3 --- /dev/null +++ b/src/PowerTools/CodeTemplates/ReverseEngineerCodeFirst/Mapping.tt @@ -0,0 +1,268 @@ +<# +// Simplifying assumptions based on reverse engineer rules +// - No complex types +// - One entity container +// - No inheritance +// - Always have two navigation properties +// - All associations expose FKs (except many:many) +#> +<#@ template hostspecific="true" language="C#" #> +<#@ include file="EF.Utility.CS.ttinclude" #><#@ + output extension=".cs" #><# + + var efHost = (EfTextTemplateHost)Host; + var code = new CodeGenerationTools(this); + + if (efHost.EntityFrameworkVersion >= new Version(4, 4)) + { +#> +using System.ComponentModel.DataAnnotations.Schema; +<# + } + else + { +#> +using System.ComponentModel.DataAnnotations; +<# + } +#> +using System.Data.Entity.ModelConfiguration; + +namespace <#= code.EscapeNamespace(efHost.Namespace) #> +{ + public class <#= efHost.EntityType.Name #>Map : EntityTypeConfiguration<<#= efHost.EntityType.Name #>> + { + public <#= efHost.EntityType.Name #>Map() + { + // Primary Key +<# + if (efHost.EntityType.KeyMembers.Count() == 1) + { +#> + this.HasKey(t => t.<#= efHost.EntityType.KeyMembers.Single().Name #>); +<# + } + else + { +#> + this.HasKey(t => new { <#= string.Join(", ", efHost.EntityType.KeyMembers.Select(m => "t." + m.Name)) #> }); +<# + } +#> + + // Properties +<# + foreach (var prop in efHost.EntityType.Properties) + { + var type = (PrimitiveType)prop.TypeUsage.EdmType; + var isKey = efHost.EntityType.KeyMembers.Contains(prop); + var storeProp = efHost.PropertyToColumnMappings[prop]; + var sgpFacet = storeProp.TypeUsage.Facets.SingleOrDefault(f => f.Name == "StoreGeneratedPattern"); + var storeGeneratedPattern = sgpFacet == null + ? StoreGeneratedPattern.None + : (StoreGeneratedPattern)sgpFacet.Value; + + var configLines = new List(); + + if (type.ClrEquivalentType == typeof(int) + || type.ClrEquivalentType == typeof(decimal) + || type.ClrEquivalentType == typeof(short) + || type.ClrEquivalentType == typeof(long)) + { + if (isKey && storeGeneratedPattern != StoreGeneratedPattern.Identity) + { + configLines.Add(".HasDatabaseGeneratedOption(DatabaseGeneratedOption.None)"); + } + else if ((!isKey || efHost.EntityType.KeyMembers.Count > 1) && storeGeneratedPattern == StoreGeneratedPattern.Identity) + { + configLines.Add(".HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)"); + } + } + + if (type.ClrEquivalentType == typeof(string) + || type.ClrEquivalentType == typeof(byte[])) + { + if (!prop.Nullable) + { + configLines.Add(".IsRequired()"); + } + + var unicodeFacet = (Facet)prop.TypeUsage.Facets.SingleOrDefault(f => f.Name == "IsUnicode"); + if(unicodeFacet != null && !(bool)unicodeFacet.Value) + { + configLines.Add(".IsUnicode(false)"); + } + + var fixedLengthFacet = (Facet)prop.TypeUsage.Facets.SingleOrDefault(f => f.Name == "FixedLength"); + if (fixedLengthFacet != null && (bool)fixedLengthFacet.Value) + { + configLines.Add(".IsFixedLength()"); + } + + var maxLengthFacet = (Facet)prop.TypeUsage.Facets.SingleOrDefault(f => f.Name == "MaxLength"); + if (maxLengthFacet != null && !maxLengthFacet.IsUnbounded) + { + configLines.Add(string.Format(".HasMaxLength({0})", maxLengthFacet.Value)); + + if (storeGeneratedPattern == StoreGeneratedPattern.Computed + && type.ClrEquivalentType == typeof(byte[]) + && (int)maxLengthFacet.Value == 8) + { + configLines.Add(".IsRowVersion()"); + } + } + } + + if(configLines.Any()) + { +#> + this.Property(t => t.<#= prop.Name #>) + <#= string.Join("\r\n ", configLines) #>; + +<# + } + } + + var tableSet = efHost.TableSet; + var tableName = (string)tableSet.MetadataProperties["Table"].Value + ?? tableSet.Name; + var schemaName = (string)tableSet.MetadataProperties["Schema"].Value; +#> + // Table & Column Mappings +<# + if (schemaName == "dbo" || string.IsNullOrWhiteSpace(schemaName)) + { +#> + this.ToTable("<#= tableName #>"); +<# + } + else + { +#> + this.ToTable("<#= tableName #>", "<#= schemaName #>"); +<# + } + + foreach (var property in efHost.EntityType.Properties) + { +#> + this.Property(t => t.<#= property.Name #>).HasColumnName("<#= efHost.PropertyToColumnMappings[property].Name #>"); +<# + } + + // Find m:m relationshipsto configure + var manyManyRelationships = efHost.EntityType.NavigationProperties + .Where(np => np.DeclaringType == efHost.EntityType + && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many + && np.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many + && np.RelationshipType.RelationshipEndMembers.First() == np.FromEndMember); // <- ensures we only configure from one end + + // Find FK relationships that this entity is the dependent of + var fkRelationships = efHost.EntityType.NavigationProperties + .Where(np => np.DeclaringType == efHost.EntityType + && ((AssociationType)np.RelationshipType).IsForeignKey + && ((AssociationType)np.RelationshipType).ReferentialConstraints.Single().ToRole == np.FromEndMember); + + if(manyManyRelationships.Any() || fkRelationships.Any()) + { +#> + + // Relationships +<# + foreach (var navProperty in manyManyRelationships) + { + var otherNavProperty = navProperty.ToEndMember.GetEntityType().NavigationProperties.Where(n => n.RelationshipType == navProperty.RelationshipType && n != navProperty).Single(); + var association = (AssociationType)navProperty.RelationshipType; + var mapping = efHost.ManyToManyMappings[association]; + var item1 = mapping.Item1; + var mappingTableName = (string)mapping.Item1.MetadataProperties["Table"].Value + ?? item1.Name; + var mappingSchemaName = (string)item1.MetadataProperties["Schema"].Value; + + // Need to ensure that FKs are decalred in the same order as the PK properties on each principal type + var leftType = (EntityType)navProperty.DeclaringType; + var leftKeyMappings = mapping.Item2[navProperty.FromEndMember]; + var leftColumns = string.Join(", ", leftType.KeyMembers.Select(m => "\"" + leftKeyMappings[m] + "\"")); + var rightType = (EntityType)otherNavProperty.DeclaringType; + var rightKeyMappings = mapping.Item2[otherNavProperty.FromEndMember]; + var rightColumns = string.Join(", ", rightType.KeyMembers.Select(m => "\"" + rightKeyMappings[m] + "\"")); +#> + this.HasMany(t => t.<#= code.Escape(navProperty) #>) + .WithMany(t => t.<#= code.Escape(otherNavProperty) #>) + .Map(m => + { +<# + if (mappingSchemaName == "dbo" || string.IsNullOrWhiteSpace(mappingSchemaName)) + { +#> + m.ToTable("<#= mappingTableName #>"); +<# + } + else + { +#> + m.ToTable("<#= mappingTableName #>", "<#= mappingSchemaName #>"); +<# + } +#> + m.MapLeftKey(<#= leftColumns #>); + m.MapRightKey(<#= rightColumns #>); + }); + +<# + } + + foreach (var navProperty in fkRelationships) + { + var otherNavProperty = navProperty.ToEndMember.GetEntityType().NavigationProperties.Where(n => n.RelationshipType == navProperty.RelationshipType && n != navProperty).Single(); + var association = (AssociationType)navProperty.RelationshipType; + + if (navProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One) + { +#> + this.HasRequired(t => t.<#= code.Escape(navProperty) #>) +<# + } + else + { +#> + this.HasOptional(t => t.<#= code.Escape(navProperty) #>) +<# + } + + if(navProperty.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) + { +#> + .WithMany(t => t.<#= code.Escape(otherNavProperty) #>) +<# + if(association.ReferentialConstraints.Single().ToProperties.Count == 1) + { +#> + .HasForeignKey(d => d.<#= association.ReferentialConstraints.Single().ToProperties.Single().Name #>); +<# + } + else + { +#> + .HasForeignKey(d => new { <#= string.Join(", ", association.ReferentialConstraints.Single().ToProperties.Select(p => "d." + p.Name)) #> }); +<# + } + } + else + { + // NOTE: We can assume that this is a required:optional relationship + // as EDMGen will never create an optional:optional relationship + // because everything is one:many except PK-PK relationships which must be required +#> + .WithOptional(t => t.<#= code.Escape(otherNavProperty) #>); +<# + } + } +#> + +<# + } +#> + } + } +} diff --git a/src/PowerTools/DbContextPackage.cs b/src/PowerTools/DbContextPackage.cs new file mode 100644 index 00000000..f5aa5f37 --- /dev/null +++ b/src/PowerTools/DbContextPackage.cs @@ -0,0 +1,527 @@ +namespace Microsoft.DbContextPackage +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.Design; + using System.Configuration; + using System.Diagnostics.Contracts; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Runtime.InteropServices; + using EnvDTE; + using EnvDTE80; + using Microsoft.DbContextPackage.Extensions; + using Microsoft.DbContextPackage.Handlers; + using Microsoft.DbContextPackage.Resources; + using Microsoft.DbContextPackage.Utilities; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Shell.Design; + using Microsoft.VisualStudio.Shell.Interop; + using Configuration = System.Configuration.Configuration; + using ConfigurationManager = System.Configuration.ConfigurationManager; + + [PackageRegistration(UseManagedResourcesOnly = true)] + [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] + [ProvideMenuResource("Menus.ctmenu", 1)] + [Guid(GuidList.guidDbContextPackagePkgString)] + [ProvideAutoLoad("{f1536ef8-92ec-443c-9ed7-fdadf150da82}")] + public sealed class DbContextPackage : Package + { + private readonly AddCustomTemplatesHandler _addCustomTemplatesHandler; + private readonly OptimizeContextHandler _optimizeContextHandler; + private readonly ReverseEngineerCodeFirstHandler _reverseEngineerCodeFirstHandler; + private readonly ViewContextHandler _viewContextHandler; + private readonly ViewDdlHandler _viewDdlHandler; + + private DTE2 _dte2; + + public DbContextPackage() + { + _addCustomTemplatesHandler = new AddCustomTemplatesHandler(this); + _optimizeContextHandler = new OptimizeContextHandler(this); + _reverseEngineerCodeFirstHandler = new ReverseEngineerCodeFirstHandler(this); + _viewContextHandler = new ViewContextHandler(this); + _viewDdlHandler = new ViewDdlHandler(this); + } + + internal DTE2 DTE2 + { + get { return _dte2; } + } + + protected override void Initialize() + { + base.Initialize(); + + _dte2 = GetService(typeof(DTE)) as DTE2; + + if (_dte2 == null) + { + return; + } + + var oleMenuCommandService + = GetService(typeof(IMenuCommandService)) as OleMenuCommandService; + + if (oleMenuCommandService != null) + { + var menuCommandID1 = new CommandID(GuidList.guidDbContextPackageCmdSet, (int)PkgCmdIDList.cmdidViewEntityDataModel); + var menuItem1 = new OleMenuCommand(OnItemContextMenuInvokeHandler, null, OnItemMenuBeforeQueryStatus, menuCommandID1); + + oleMenuCommandService.AddCommand(menuItem1); + + var menuCommandID2 = new CommandID(GuidList.guidDbContextPackageCmdSet, (int)PkgCmdIDList.cmdidViewEntityDataModelXml); + var menuItem2 = new OleMenuCommand(OnItemContextMenuInvokeHandler, null, OnItemMenuBeforeQueryStatus, menuCommandID2); + + oleMenuCommandService.AddCommand(menuItem2); + + var menuCommandID3 = new CommandID(GuidList.guidDbContextPackageCmdSet, (int)PkgCmdIDList.cmdidPrecompileEntityDataModelViews); + var menuItem3 = new OleMenuCommand(OnOptimizeContextInvokeHandler, null, OnOptimizeContextBeforeQueryStatus, menuCommandID3); + + oleMenuCommandService.AddCommand(menuItem3); + + var menuCommandID4 = new CommandID(GuidList.guidDbContextPackageCmdSet, (int)PkgCmdIDList.cmdidViewEntityModelDdl); + var menuItem4 = new OleMenuCommand(OnItemContextMenuInvokeHandler, null, OnItemMenuBeforeQueryStatus, menuCommandID4); + + oleMenuCommandService.AddCommand(menuItem4); + + var menuCommandID5 = new CommandID(GuidList.guidDbContextPackageCmdSet, (int)PkgCmdIDList.cmdidReverseEngineerCodeFirst); + var menuItem5 = new OleMenuCommand(OnProjectContextMenuInvokeHandler, null, OnProjectMenuBeforeQueryStatus, menuCommandID5); + + oleMenuCommandService.AddCommand(menuItem5); + + var menuCommandID6 = new CommandID(GuidList.guidDbContextPackageCmdSet, (int)PkgCmdIDList.cmdidCustomizeReverseEngineerTemplates); + var menuItem6 = new OleMenuCommand(OnProjectContextMenuInvokeHandler, null, OnProjectMenuBeforeQueryStatus, menuCommandID6); + + oleMenuCommandService.AddCommand(menuItem6); + } + } + + private void OnProjectMenuBeforeQueryStatus(object sender, EventArgs e) + { + var menuCommand = sender as MenuCommand; + + if (menuCommand == null) + { + return; + } + + if (_dte2.SelectedItems.Count != 1) + { + return; + } + + var project = _dte2.SelectedItems.Item(1).Project; + + if (project == null) + { + return; + } + + menuCommand.Visible = + project.Kind == "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; // csproj + } + + private void OnItemMenuBeforeQueryStatus(object sender, EventArgs e) + { + OnItemMenuBeforeQueryStatus( + sender, + new[] { FileExtensions.CSharp, FileExtensions.VisualBasic }); + } + + private void OnOptimizeContextBeforeQueryStatus(object sender, EventArgs e) + { + OnItemMenuBeforeQueryStatus( + sender, + new[] { FileExtensions.CSharp, FileExtensions.VisualBasic, FileExtensions.EntityDataModel }); + } + + private void OnItemMenuBeforeQueryStatus(object sender, IEnumerable supportedExtensions) + { + Contract.Requires(supportedExtensions != null); + + var menuCommand = sender as MenuCommand; + + if (menuCommand == null) + { + return; + } + + if (_dte2.SelectedItems.Count != 1) + { + return; + } + + var extensionValue = GetSelectedItemExtension(); + menuCommand.Visible = supportedExtensions.Contains(extensionValue); + } + + private string GetSelectedItemExtension() + { + var selectedItem = _dte2.SelectedItems.Item(1); + + if ((selectedItem.ProjectItem == null) + || (selectedItem.ProjectItem.Properties == null)) + { + return null; + } + + var extension = selectedItem.ProjectItem.Properties.Item("Extension"); + + if (extension == null) + { + return null; + } + + return (string)extension.Value; + } + + private void OnProjectContextMenuInvokeHandler(object sender, EventArgs e) + { + var menuCommand = sender as MenuCommand; + if (menuCommand == null || _dte2.SelectedItems.Count != 1) + { + return; + } + + var project = _dte2.SelectedItems.Item(1).Project; + if (project == null) + { + return; + } + + if (menuCommand.CommandID.ID == PkgCmdIDList.cmdidReverseEngineerCodeFirst) + { + _reverseEngineerCodeFirstHandler.ReverseEngineerCodeFirst(project); + } + else if (menuCommand.CommandID.ID == PkgCmdIDList.cmdidCustomizeReverseEngineerTemplates) + { + _addCustomTemplatesHandler.AddCustomTemplates(project); + } + } + + private void OnItemContextMenuInvokeHandler(object sender, EventArgs e) + { + var menuCommand = sender as MenuCommand; + + if (menuCommand == null) + { + return; + } + + if (_dte2.SelectedItems.Count != 1) + { + return; + } + + Type systemContextType; + var context = DiscoverUserContextType(out systemContextType); + + if (context != null) + { + if (menuCommand.CommandID.ID == PkgCmdIDList.cmdidPrecompileEntityDataModelViews) + { + _optimizeContextHandler.OptimizeContext(context); + } + else if (menuCommand.CommandID.ID == PkgCmdIDList.cmdidViewEntityModelDdl) + { + _viewDdlHandler.ViewDdl(context); + } + else + { + _viewContextHandler.ViewContext(menuCommand, context, systemContextType); + } + } + } + + private void OnOptimizeContextInvokeHandler(object sender, EventArgs e) + { + if (_dte2.SelectedItems.Count != 1) + { + return; + } + + var extensionValue = GetSelectedItemExtension(); + + if (extensionValue != FileExtensions.EntityDataModel) + { + OnItemContextMenuInvokeHandler(sender, e); + + return; + } + + _optimizeContextHandler.OptimizeEdmx( + (string)_dte2.SelectedItems.Item(1).ProjectItem.Properties.Item("FullPath").Value); + } + + internal static dynamic GetObjectContext(dynamic context) + { + var objectContextAdapterType + = context.GetType().GetInterface("System.Data.Entity.Infrastructure.IObjectContextAdapter"); + + return objectContextAdapterType.InvokeMember("ObjectContext", BindingFlags.GetProperty, null, context, null); + } + + internal void LogError(string statusMessage, Exception exception) + { + Contract.Requires(!string.IsNullOrWhiteSpace(statusMessage)); + Contract.Requires(exception != null); + + var edmSchemaErrorException = exception as EdmSchemaErrorException; + var compilerErrorException = exception as CompilerErrorException; + + _dte2.StatusBar.Text = statusMessage; + + var buildOutputWindow = _dte2.ToolWindows.OutputWindow.OutputWindowPanes.Item("Build"); + + buildOutputWindow.OutputString(Environment.NewLine); + + if (edmSchemaErrorException != null) + { + buildOutputWindow.OutputString(edmSchemaErrorException.Message + Environment.NewLine); + + foreach (var error in edmSchemaErrorException.Errors) + { + buildOutputWindow.OutputString(error + Environment.NewLine); + } + } + else if (compilerErrorException != null) + { + buildOutputWindow.OutputString(compilerErrorException.Message + Environment.NewLine); + + foreach (var error in compilerErrorException.Errors) + { + buildOutputWindow.OutputString(error + Environment.NewLine); + } + } + else + { + buildOutputWindow.OutputString(exception + Environment.NewLine); + } + + buildOutputWindow.Activate(); + } + + private dynamic DiscoverUserContextType(out Type systemContextType) + { + var project = _dte2.SelectedItems.Item(1).ProjectItem.ContainingProject; + + if (!project.TryBuild()) + { + _dte2.StatusBar.Text = Strings.BuildFailed; + systemContextType = null; + + return null; + } + + DynamicTypeService typeService; + IVsSolution solution; + using (var serviceProvider = new ServiceProvider((VisualStudio.OLE.Interop.IServiceProvider)_dte2.DTE)) + { + typeService = (DynamicTypeService)serviceProvider.GetService(typeof(DynamicTypeService)); + solution = (IVsSolution)serviceProvider.GetService(typeof(SVsSolution)); + } + + IVsHierarchy vsHierarchy; + var hr = solution.GetProjectOfUniqueName(_dte2.SelectedItems.Item(1).ProjectItem.ContainingProject.UniqueName, out vsHierarchy); + + if (hr != ProjectExtensions.S_OK) + { + throw Marshal.GetExceptionForHR(hr); + } + + var resolver = typeService.GetTypeResolutionService(vsHierarchy); + + systemContextType = resolver.GetType("System.Data.Entity.DbContext"); + + if (systemContextType != null) + { + var codeElements = FindClassesInCodeModel(_dte2.SelectedItems.Item(1).ProjectItem.FileCodeModel.CodeElements); + + if (codeElements.Any()) + { + var contextInfoType = systemContextType.Assembly.GetType("System.Data.Entity.Infrastructure.DbContextInfo"); + var startUpProject = GetStartUpProject() ?? project; + Configuration userConfig; + + try + { + userConfig = GetUserConfig(startUpProject); + } + catch (Exception ex) + { + LogError(Strings.LoadConfigFailed, ex); + + return null; + } + + SetDataDirectory(startUpProject); + + foreach (var codeElement in codeElements) + { + var userContextType = resolver.GetType(codeElement.FullName); + + if (userContextType != null && systemContextType.IsAssignableFrom(userContextType)) + { + dynamic contextInfo; + + if (contextInfoType != null) + { + var constructor = contextInfoType.GetConstructor(new[] { typeof(Type), typeof(Configuration) }); + + if (constructor != null) + { + // Versions 4.3.0 and higher + contextInfo = constructor.Invoke(new object[] { userContextType, userConfig }); + } + else + { + constructor = contextInfoType.GetConstructor(new[] { typeof(Type), typeof(ConnectionStringSettingsCollection) }); + Contract.Assert(constructor != null); + + // Versions 4.1.10715 through 4.2.0.0 + contextInfo = constructor.Invoke(new object[] { userContextType, userConfig.ConnectionStrings.ConnectionStrings }); + } + } + else + { + // Versions 4.1.10331.0 and lower + throw Error.UnsupportedVersion(); + } + + if (contextInfo.IsConstructible) + { + DisableDatabaseInitializer(userContextType, systemContextType); + + try + { + return contextInfo.CreateInstance(); + } + catch (Exception exception) + { + LogError(Strings.CreateContextFailed(userContextType.Name), exception); + + return null; + } + } + } + } + } + } + + _dte2.StatusBar.Text = Strings.NoContext; + + return null; + } + + private Project GetStartUpProject() + { + var startupProjectPaths = (object[])_dte2.Solution.SolutionBuild.StartupProjects; + + if (startupProjectPaths.Length == 1) + { + var startupProjectPath = (string)startupProjectPaths[0]; + + if (!Path.IsPathRooted(startupProjectPath)) + { + var solutionPath = Path.GetDirectoryName((string)_dte2.Solution.Properties.Item("Path").Value); + startupProjectPath = Path.Combine( + solutionPath, + startupProjectPath); + } + + return _dte2.Solution.Projects.Cast().Single( + p => + { + string fullName; + try + { + fullName = p.FullName; + } + catch (NotImplementedException) + { + return false; + } + + return fullName == startupProjectPath; + }); + } + + return null; + } + + private static Configuration GetUserConfig(Project project) + { + Contract.Requires(project != null); + + var userConfigFilename + = Path.Combine( + (string)project.Properties.Item("FullPath").Value, + project.IsWebProject() + ? "Web.config" + : "App.config"); + + return ConfigurationManager.OpenMappedExeConfiguration( + new ExeConfigurationFileMap { ExeConfigFilename = userConfigFilename }, + ConfigurationUserLevel.None); + } + + private static void SetDataDirectory(Project project) + { + Contract.Requires(project != null); + + AppDomain.CurrentDomain.SetData( + "DataDirectory", + project.IsWebProject() + ? Path.Combine(project.GetProjectDir(), "App_Data") + : project.GetTargetDir()); + } + + private static IEnumerable FindClassesInCodeModel(CodeElements codeElements) + { + Contract.Requires(codeElements != null); + + foreach (CodeElement codeElement in codeElements) + { + if (codeElement.Kind == vsCMElement.vsCMElementClass) + { + yield return codeElement; + } + + foreach (var element in FindClassesInCodeModel(codeElement.Children)) + { + yield return element; + } + } + } + + private static void DisableDatabaseInitializer(Type userContextType, Type systemContextType) + { + Contract.Requires(userContextType != null); + Contract.Requires(systemContextType != null); + + var databaseType = systemContextType.Assembly.GetType("System.Data.Entity.Database"); + + if (databaseType != null) + { + var setInitializerMethodInfo + = databaseType.GetMethod("SetInitializerInternal", BindingFlags.NonPublic | BindingFlags.Static); + + if (setInitializerMethodInfo != null) + { + var boundSetInitializerMethodInfo + = setInitializerMethodInfo.MakeGenericMethod(userContextType); + + boundSetInitializerMethodInfo.Invoke(null, new object[] { null, true }); + } + } + } + + internal T GetService() + where T : class + { + return (T)GetService(typeof(T)); + } + } +} \ No newline at end of file diff --git a/src/PowerTools/DbContextPackage.vsct b/src/PowerTools/DbContextPackage.vsct new file mode 100644 index 00000000..84bf1385 --- /dev/null +++ b/src/PowerTools/DbContextPackage.vsct @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Entity Framework + Entity Framework + + + + + + + Entity Framework + Entity Framework + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PowerTools/Extensions/CompilerErrorCollectionExtensions.cs b/src/PowerTools/Extensions/CompilerErrorCollectionExtensions.cs new file mode 100644 index 00000000..7a11404c --- /dev/null +++ b/src/PowerTools/Extensions/CompilerErrorCollectionExtensions.cs @@ -0,0 +1,22 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System.CodeDom.Compiler; + using System.Diagnostics.Contracts; + using System.Linq; + + internal static class CompilerErrorCollectionExtensions + { + public static void HandleErrors(this CompilerErrorCollection errors, string message) + { + Contract.Requires(errors != null); + Contract.Requires(!string.IsNullOrWhiteSpace(message)); + + if (errors.HasErrors) + { + throw new CompilerErrorException( + message, + errors.Cast().ToList()); + } + } + } +} diff --git a/src/PowerTools/Extensions/CompilerErrorException.cs b/src/PowerTools/Extensions/CompilerErrorException.cs new file mode 100644 index 00000000..df295c5e --- /dev/null +++ b/src/PowerTools/Extensions/CompilerErrorException.cs @@ -0,0 +1,58 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Runtime.Serialization; + + [Serializable] + public class CompilerErrorException : Exception + { + private readonly IEnumerable _errors; + + public CompilerErrorException() + { + } + + public CompilerErrorException(string message) + : base(message) + { + } + + public CompilerErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + + public CompilerErrorException(string message, IEnumerable errors) + : base(message) + { + Contract.Requires(errors != null); + + _errors = errors; + } + + protected CompilerErrorException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Contract.Requires(info != null); + + _errors = (IEnumerable)info.GetValue("Errors", typeof(IEnumerable)); + } + + public IEnumerable Errors + { + get { return _errors; } + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + Contract.Requires(info != null); + + info.AddValue("Errors", _errors); + + base.GetObjectData(info, context); + } + } +} diff --git a/src/PowerTools/Extensions/EdmSchemaErrorException.cs b/src/PowerTools/Extensions/EdmSchemaErrorException.cs new file mode 100644 index 00000000..c6d28942 --- /dev/null +++ b/src/PowerTools/Extensions/EdmSchemaErrorException.cs @@ -0,0 +1,58 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + using System.Collections.Generic; + using System.Data.Metadata.Edm; + using System.Diagnostics.Contracts; + using System.Runtime.Serialization; + + [Serializable] + public class EdmSchemaErrorException : Exception + { + private readonly IEnumerable _errors; + + public EdmSchemaErrorException() + { + } + + public EdmSchemaErrorException(string message) + : base(message) + { + } + + public EdmSchemaErrorException(string message, IEnumerable errors) + : base(message) + { + Contract.Requires(errors != null); + + _errors = errors; + } + + public EdmSchemaErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected EdmSchemaErrorException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Contract.Requires(info != null); + + _errors = (IEnumerable)info.GetValue("Errors", typeof(IEnumerable)); + } + + public IEnumerable Errors + { + get { return _errors; } + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + Contract.Requires(info != null); + + info.AddValue("Errors", _errors); + + base.GetObjectData(info, context); + } + } +} diff --git a/src/PowerTools/Extensions/IComponentModelExtensions.cs b/src/PowerTools/Extensions/IComponentModelExtensions.cs new file mode 100644 index 00000000..98a6ee91 --- /dev/null +++ b/src/PowerTools/Extensions/IComponentModelExtensions.cs @@ -0,0 +1,19 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + using System.Diagnostics.Contracts; + using Microsoft.VisualStudio.ComponentModelHost; + + internal static class IComponentModelExtensions + { + public static object GetService(this IComponentModel componentModel, Type serviceType) + { + Contract.Requires(componentModel != null); + Contract.Requires(serviceType != null); + + return typeof(IComponentModel).GetMethod("GetService") + .MakeGenericMethod(serviceType) + .Invoke(componentModel, null); + } + } +} diff --git a/src/PowerTools/Extensions/IEnumerableOfEdmSchemaErrorExtensions.cs b/src/PowerTools/Extensions/IEnumerableOfEdmSchemaErrorExtensions.cs new file mode 100644 index 00000000..76848469 --- /dev/null +++ b/src/PowerTools/Extensions/IEnumerableOfEdmSchemaErrorExtensions.cs @@ -0,0 +1,27 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System.Collections.Generic; + using System.Data.Metadata.Edm; + using System.Diagnostics.Contracts; + using System.Linq; + + internal static class IEnumerableOfEdmSchemaErrorExtensions + { + public static void HandleErrors(this IEnumerable errors, string message) + { + Contract.Requires(errors != null); + + if (errors.HasErrors()) + { + throw new EdmSchemaErrorException(message, errors); + } + } + + private static bool HasErrors(this IEnumerable errors) + { + Contract.Requires(errors != null); + + return errors.Any(e => e.Severity == EdmSchemaErrorSeverity.Error); + } + } +} diff --git a/src/PowerTools/Extensions/ProjectExtensions.cs b/src/PowerTools/Extensions/ProjectExtensions.cs new file mode 100644 index 00000000..e6153644 --- /dev/null +++ b/src/PowerTools/Extensions/ProjectExtensions.cs @@ -0,0 +1,158 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using EnvDTE; + using Microsoft.VisualStudio.ComponentModelHost; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Shell.Interop; + using IServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; + + internal static class ProjectExtensions + { + public const int S_OK = 0; + public const string WebApplicationProjectTypeGuid = "{349C5851-65DF-11DA-9384-00065B846F21}"; + public const string WebSiteProjectTypeGuid = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"; + + public static ProjectItem AddNewFile(this Project project, string path, string contents) + { + Contract.Requires(project != null); + Contract.Requires(!string.IsNullOrWhiteSpace(path)); + + if (string.IsNullOrWhiteSpace(contents)) + { + return null; + } + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + project.DTE.SourceControl.CheckOutItemIfNeeded(path); + File.WriteAllText(path, contents); + + return project.ProjectItems.AddFromFile(path); + } + + public static string GetProjectDir(this Project project) + { + Contract.Requires(project != null); + + return project.GetPropertyValue("FullPath"); + } + + public static string GetTargetDir(this Project project) + { + Contract.Requires(project != null); + + var fullPath = project.GetProjectDir(); + string outputPath; + + outputPath = project.GetConfigurationPropertyValue("OutputPath"); + + return Path.Combine(fullPath, outputPath); + } + + public static void InstallPackage(this Project project, string packageId) + { + Contract.Requires(project != null); + Contract.Requires(!string.IsNullOrWhiteSpace(packageId)); + + var typeNuGetConstants = Type.GetType("NuGet.NuGetConstants, NuGet.VisualStudio", true); + var typeIVsPackageInstaller = Type.GetType("NuGet.VisualStudio.IVsPackageInstaller, NuGet.VisualStudio", true); + var typeSemanticVersion = Type.GetType("NuGet.SemanticVersion, NuGet.Core", true); + + var componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel)); + var packageInstaller = componentModel.GetService(typeIVsPackageInstaller); + var source = (string)typeNuGetConstants.GetField("DefaultFeedUrl").GetValue(null); + + typeIVsPackageInstaller.GetMethod( + "InstallPackage", + new[] { typeof(string), typeof(Project), typeof(string), typeSemanticVersion, typeof(bool) }) + .Invoke(packageInstaller, new object[] { source, project, packageId, null, false }); + } + + public static bool IsWebProject(this Project project) + { + Contract.Requires(project != null); + + return project.GetProjectTypes().Any( + g => g.EqualsIgnoreCase(WebApplicationProjectTypeGuid) + || g.EqualsIgnoreCase(WebSiteProjectTypeGuid)); + } + + public static bool TryBuild(this Project project) + { + Contract.Requires(project != null); + + var dte = project.DTE; + var configuration = dte.Solution.SolutionBuild.ActiveConfiguration.Name; + + dte.Solution.SolutionBuild.BuildProject(configuration, project.UniqueName, true); + + return dte.Solution.SolutionBuild.LastBuildInfo == 0; + } + + private static T GetPropertyValue(this Project project, string propertyName) + { + Contract.Requires(project != null); + Contract.Requires(!string.IsNullOrWhiteSpace(propertyName)); + + var property = project.Properties.Item(propertyName); + + if (property == null) + { + return default(T); + } + + return (T)property.Value; + } + + private static T GetConfigurationPropertyValue(this Project project, string propertyName) + { + Contract.Requires(project != null); + Contract.Requires(!string.IsNullOrWhiteSpace(propertyName)); + + var property = project.ConfigurationManager.ActiveConfiguration.Properties.Item(propertyName); + + if (property == null) + { + return default(T); + } + + return (T)property.Value; + } + + private static IEnumerable GetProjectTypes(this Project project) + { + Contract.Requires(project != null); + + IVsSolution solution; + using (var serviceProvider = new ServiceProvider((IServiceProvider)project.DTE)) + { + solution = (IVsSolution)serviceProvider.GetService(typeof(IVsSolution)); + } + + IVsHierarchy hierarchy; + var hr = solution.GetProjectOfUniqueName(project.UniqueName, out hierarchy); + + if (hr != S_OK) + { + Marshal.ThrowExceptionForHR(hr); + } + + string projectTypeGuidsString; + + var aggregatableProject = (IVsAggregatableProject)hierarchy; + hr = aggregatableProject.GetAggregateProjectTypeGuids(out projectTypeGuidsString); + + if (hr != S_OK) + { + Marshal.ThrowExceptionForHR(hr); + } + + return projectTypeGuidsString.Split(';'); + } + } +} \ No newline at end of file diff --git a/src/PowerTools/Extensions/ProjectItemsExtensions.cs b/src/PowerTools/Extensions/ProjectItemsExtensions.cs new file mode 100644 index 00000000..8b36382c --- /dev/null +++ b/src/PowerTools/Extensions/ProjectItemsExtensions.cs @@ -0,0 +1,21 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + using System.Diagnostics.Contracts; + using System.Linq; + using EnvDTE; + + internal static class ProjectItemsExtensions + { + public static ProjectItem GetItem(this ProjectItems projectItems, string name) + { + Contract.Requires(projectItems != null); + Contract.Requires(!string.IsNullOrWhiteSpace(name)); + + return projectItems + .Cast() + .FirstOrDefault( + pi => string.Equals(pi.Name, name, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/src/PowerTools/Extensions/SourceControlExtenstions.cs b/src/PowerTools/Extensions/SourceControlExtenstions.cs new file mode 100644 index 00000000..1a0de588 --- /dev/null +++ b/src/PowerTools/Extensions/SourceControlExtenstions.cs @@ -0,0 +1,21 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System.Diagnostics.Contracts; + using EnvDTE; + + internal static class SourceControlExtenstions + { + public static bool CheckOutItemIfNeeded(this SourceControl sourceControl, string itemName) + { + Contract.Requires(sourceControl != null); + Contract.Requires(!string.IsNullOrWhiteSpace(itemName)); + + if (sourceControl.IsItemUnderSCC(itemName) && !sourceControl.IsItemCheckedOut(itemName)) + { + return sourceControl.CheckOutItem(itemName); + } + + return false; + } + } +} diff --git a/src/PowerTools/Extensions/StringExtensions.cs b/src/PowerTools/Extensions/StringExtensions.cs new file mode 100644 index 00000000..715eb00a --- /dev/null +++ b/src/PowerTools/Extensions/StringExtensions.cs @@ -0,0 +1,12 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + + internal static class StringExtensions + { + public static bool EqualsIgnoreCase(this string s1, string s2) + { + return string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/PowerTools/Extensions/XContainerExtensions.cs b/src/PowerTools/Extensions/XContainerExtensions.cs new file mode 100644 index 00000000..2c092fc6 --- /dev/null +++ b/src/PowerTools/Extensions/XContainerExtensions.cs @@ -0,0 +1,29 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Xml.Linq; + + internal static class XContainerExtensions + { + /// + /// Gets the first (in document order) child element with the specified local name and one of the namespaces. + /// + /// The node containing the child elements. + /// A collection of namespaces used when searching for the child element. + /// The local (unqualified) name to match. + /// A that matches the specified name and namespace, or null. + public static XElement Element(this XContainer container, IEnumerable namespaces, string localName) + { + Contract.Requires(container != null); + Contract.Requires(namespaces != null); + Contract.Requires(!string.IsNullOrWhiteSpace(localName)); + + return container.Elements() + .FirstOrDefault( + e => e.Name.LocalName == localName + && namespaces.Contains(e.Name.Namespace)); + } + } +} diff --git a/src/PowerTools/GlobalSuppressions.cs b/src/PowerTools/GlobalSuppressions.cs new file mode 100644 index 00000000..746e3dae --- /dev/null +++ b/src/PowerTools/GlobalSuppressions.cs @@ -0,0 +1,11 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. Project-level +// suppressions either have no target or are given a specific target +// and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Error List, point to "Suppress Message(s)", and click "In Project +// Suppression File". You do not need to add suppressions to this +// file manually. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1017:MarkAssembliesWithComVisible")] diff --git a/src/PowerTools/Guids.cs b/src/PowerTools/Guids.cs new file mode 100644 index 00000000..dc33b9d7 --- /dev/null +++ b/src/PowerTools/Guids.cs @@ -0,0 +1,14 @@ +// Guids.cs +// MUST match guids.h +namespace Microsoft.DbContextPackage +{ + using System; + + internal static class GuidList + { + public const string guidDbContextPackagePkgString = "2b119c79-9836-46e2-b5ed-eb766cebbf7c"; + public const string guidDbContextPackageCmdSetString = "c769a05d-8d51-4919-bfe6-5f35a0eaf27e"; + + public static readonly Guid guidDbContextPackageCmdSet = new Guid(guidDbContextPackageCmdSetString); + } +} \ No newline at end of file diff --git a/src/PowerTools/Handlers/AddCustomTemplatesHandler.cs b/src/PowerTools/Handlers/AddCustomTemplatesHandler.cs new file mode 100644 index 00000000..7e04f8e4 --- /dev/null +++ b/src/PowerTools/Handlers/AddCustomTemplatesHandler.cs @@ -0,0 +1,51 @@ +namespace Microsoft.DbContextPackage.Handlers +{ + using System; + using System.Diagnostics.Contracts; + using System.IO; + using EnvDTE; + using Microsoft.DbContextPackage.Extensions; + using Microsoft.DbContextPackage.Resources; + using Microsoft.DbContextPackage.Utilities; + + internal class AddCustomTemplatesHandler + { + private readonly DbContextPackage _package; + + public AddCustomTemplatesHandler(DbContextPackage package) + { + Contract.Requires(package != null); + + _package = package; + } + + public void AddCustomTemplates(Project project) + { + Contract.Requires(project != null); + + try + { + AddTemplate(project, Templates.ContextTemplate); + AddTemplate(project, Templates.EntityTemplate); + AddTemplate(project, Templates.MappingTemplate); + } + catch (Exception ex) + { + _package.LogError(Strings.AddTemplatesError, ex); + } + } + + private static void AddTemplate(Project project, string templatePath) + { + Contract.Requires(project != null); + Contract.Requires(!string.IsNullOrWhiteSpace(templatePath)); + + var projectDir = project.GetProjectDir(); + + var filePath = Path.Combine(projectDir, templatePath); + var contents = Templates.GetDefaultTemplate(templatePath); + var item = project.AddNewFile(filePath, contents); + item.Properties.Item("CustomTool").Value = null; + } + } +} diff --git a/src/PowerTools/Handlers/OptimizeContextHandler.cs b/src/PowerTools/Handlers/OptimizeContextHandler.cs new file mode 100644 index 00000000..cd3b274e --- /dev/null +++ b/src/PowerTools/Handlers/OptimizeContextHandler.cs @@ -0,0 +1,140 @@ +namespace Microsoft.DbContextPackage.Handlers +{ + using System; + using System.Data.Entity.Design; + using System.Data.Mapping; + using System.Data.Metadata.Edm; + using System.Diagnostics.Contracts; + using System.IO; + using System.Threading.Tasks; + using System.Windows.Forms; + using EnvDTE; + using Microsoft.DbContextPackage.Extensions; + using Microsoft.DbContextPackage.Resources; + using Microsoft.DbContextPackage.Utilities; + using Task = System.Threading.Tasks.Task; + + internal class OptimizeContextHandler + { + private readonly DbContextPackage _package; + + public OptimizeContextHandler(DbContextPackage package) + { + Contract.Requires(package != null); + + _package = package; + } + + public void OptimizeContext(dynamic context) + { + Type contextType = context.GetType(); + + try + { + var selectedItem = _package.DTE2.SelectedItems.Item(1); + var selectedItemExtension = (string)selectedItem.ProjectItem.Properties.Item("Extension").Value; + var languageOption = selectedItemExtension == FileExtensions.CSharp + ? LanguageOption.GenerateCSharpCode + : LanguageOption.GenerateVBCode; + var objectContext = DbContextPackage.GetObjectContext(context); + var mappingCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace); + + OptimizeContextCore(languageOption, contextType.Name, mappingCollection); + } + catch (Exception ex) + { + _package.LogError(Strings.Optimize_ContextError(contextType.Name), ex); + } + } + + public void OptimizeEdmx(string inputPath) + { + Contract.Requires(!string.IsNullOrWhiteSpace(inputPath)); + + var baseFileName = Path.GetFileNameWithoutExtension(inputPath); + + try + { + var project = _package.DTE2.SelectedItems.Item(1).ProjectItem.ContainingProject; + var languageOption = project.CodeModel.Language == CodeModelLanguageConstants.vsCMLanguageCSharp + ? LanguageOption.GenerateCSharpCode + : LanguageOption.GenerateVBCode; + var mappingCollection = new EdmxUtility(inputPath).GetMappingCollection(); + + OptimizeContextCore(languageOption, baseFileName, mappingCollection); + } + catch (Exception ex) + { + _package.LogError(Strings.Optimize_EdmxError(baseFileName), ex); + } + } + + private void OptimizeContextCore(LanguageOption languageOption, string baseFileName, StorageMappingItemCollection mappingCollection) + { + Contract.Requires(!string.IsNullOrWhiteSpace(baseFileName)); + Contract.Requires(mappingCollection != null); + + var progressTimer = new Timer { Interval = 1000 }; + + try + { + var selectedItem = _package.DTE2.SelectedItems.Item(1); + var selectedItemPath = (string)selectedItem.ProjectItem.Properties.Item("FullPath").Value; + var viewGenerator = new EntityViewGenerator(languageOption); + var viewsFileName = baseFileName + + ".Views" + + ((languageOption == LanguageOption.GenerateCSharpCode) + ? FileExtensions.CSharp + : FileExtensions.VisualBasic); + var viewsPath = Path.Combine( + Path.GetDirectoryName(selectedItemPath), + viewsFileName); + + _package.DTE2.SourceControl.CheckOutItemIfNeeded(viewsPath); + + var progress = 1; + progressTimer.Tick += (sender, e) => + { + _package.DTE2.StatusBar.Progress(true, string.Empty, progress, 100); + progress = progress == 100 ? 1 : progress + 1; + _package.DTE2.StatusBar.Text = Strings.Optimize_Begin(baseFileName); + }; + + progressTimer.Start(); + + Task.Factory.StartNew( + () => + { + var errors = viewGenerator.GenerateViews(mappingCollection, viewsPath); + errors.HandleErrors(Strings.Optimize_SchemaError(baseFileName)); + }) + .ContinueWith( + t => + { + progressTimer.Stop(); + _package.DTE2.StatusBar.Progress(false); + + if (t.IsFaulted) + { + _package.LogError(Strings.Optimize_Error(baseFileName), t.Exception); + + return; + } + + selectedItem.ProjectItem.ContainingProject.ProjectItems.AddFromFile(viewsPath); + _package.DTE2.ItemOperations.OpenFile(viewsPath); + + _package.DTE2.StatusBar.Text = Strings.Optimize_End(baseFileName, Path.GetFileName(viewsPath)); + }, + TaskScheduler.FromCurrentSynchronizationContext()); + } + catch + { + progressTimer.Stop(); + _package.DTE2.StatusBar.Progress(false); + + throw; + } + } + } +} \ No newline at end of file diff --git a/src/PowerTools/Handlers/ReverseEngineerCodeFirstHandler.cs b/src/PowerTools/Handlers/ReverseEngineerCodeFirstHandler.cs new file mode 100644 index 00000000..67f4c18c --- /dev/null +++ b/src/PowerTools/Handlers/ReverseEngineerCodeFirstHandler.cs @@ -0,0 +1,364 @@ +namespace Microsoft.DbContextPackage.Handlers +{ + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Data.Common; + using System.Data.Entity.Design; + using System.Data.Entity.Design.PluralizationServices; + using System.Data.Metadata.Edm; + using System.Data.SqlClient; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Text; + using System.Xml; + using Microsoft.DbContextPackage.Extensions; + using Microsoft.DbContextPackage.Resources; + using Microsoft.DbContextPackage.Utilities; + using Microsoft.VisualStudio.Data.Core; + using Microsoft.VisualStudio.Data.Services; + using Microsoft.VisualStudio.Shell; + using Project = EnvDTE.Project; + + internal class ReverseEngineerCodeFirstHandler + { + private static readonly IEnumerable _storeMetadataFilters = new[] + { + new EntityStoreSchemaFilterEntry(null, null, "EdmMetadata", EntityStoreSchemaFilterObjectTypes.Table, EntityStoreSchemaFilterEffect.Exclude), + new EntityStoreSchemaFilterEntry(null, null, "__MigrationHistory", EntityStoreSchemaFilterObjectTypes.Table, EntityStoreSchemaFilterEffect.Exclude) + }; + private readonly DbContextPackage _package; + + public ReverseEngineerCodeFirstHandler(DbContextPackage package) + { + Contract.Requires(package != null); + + _package = package; + } + + public void ReverseEngineerCodeFirst(Project project) + { + Contract.Requires(project != null); + + try + { + // Show dialog with SqlClient selected by default + var dialogFactory = _package.GetService(); + var dialog = dialogFactory.CreateConnectionDialog(); + dialog.AddAllSources(); + dialog.SelectedSource = new Guid("067ea0d9-ba62-43f7-9106-34930c60c528"); + var dialogResult = dialog.ShowDialog(connect: true); + + if (dialogResult != null) + { + // Find connection string and provider + _package.DTE2.StatusBar.Text = Strings.ReverseEngineer_LoadSchema; + var connection = (DbConnection)dialogResult.GetLockedProviderObject(); + var connectionString = connection.ConnectionString; + var providerManager = (IVsDataProviderManager)Package.GetGlobalService(typeof(IVsDataProviderManager)); + IVsDataProvider dp; + providerManager.Providers.TryGetValue(dialogResult.Provider, out dp); + var providerInvariant = (string)dp.GetProperty("InvariantName"); + + // Load store schema + var storeGenerator = new EntityStoreSchemaGenerator(providerInvariant, connectionString, "dbo"); + storeGenerator.GenerateForeignKeyProperties = true; + var errors = storeGenerator.GenerateStoreMetadata(_storeMetadataFilters).Where(e => e.Severity == EdmSchemaErrorSeverity.Error); + errors.HandleErrors(Strings.ReverseEngineer_SchemaError); + + // Generate default mapping + _package.DTE2.StatusBar.Text = Strings.ReverseEngineer_GenerateMapping; + var contextName = connection.Database.Replace(" ", string.Empty).Replace(".", string.Empty) + "Context"; + var modelGenerator = new EntityModelSchemaGenerator(storeGenerator.EntityContainer, "DefaultNamespace", contextName); + modelGenerator.PluralizationService = PluralizationService.CreateService(new CultureInfo("en")); + modelGenerator.GenerateForeignKeyProperties = true; + modelGenerator.GenerateMetadata(); + + // Pull out info about types to be generated + var entityTypes = modelGenerator.EdmItemCollection.OfType().ToArray(); + var mappings = new EdmMapping(modelGenerator, storeGenerator.StoreItemCollection); + + // Find the project to add the code to + var vsProject = (VSLangProj.VSProject)project.Object; + var projectDirectory = new FileInfo(project.FileName).Directory; + var projectNamespace = (string)project.Properties.Item("RootNamespace").Value; + var references = vsProject.References.Cast(); + + if (!references.Any(r => r.Name == "EntityFramework")) + { + // Add EF References + _package.DTE2.StatusBar.Text = Strings.ReverseEngineer_InstallEntityFramework; + + try + { + project.InstallPackage("EntityFramework"); + } + catch (Exception ex) + { + _package.LogError(Strings.ReverseEngineer_InstallEntityFrameworkError, ex); + } + } + + // Generate Entity Classes and Mappings + _package.DTE2.StatusBar.Text = Strings.ReverseEngineer_GenerateClasses; + var templateProcessor = new TemplateProcessor(project); + var modelsNamespace = projectNamespace + ".Models"; + var modelsDirectory = Path.Combine(projectDirectory.FullName, "Models"); + var mappingNamespace = modelsNamespace + ".Mapping"; + var mappingDirectory = Path.Combine(modelsDirectory, "Mapping"); + var entityFrameworkVersion = GetEntityFrameworkVersion(references); + + foreach (var entityType in entityTypes) + { + // Generate the code file + var entityHost = new EfTextTemplateHost + { + EntityType = entityType, + EntityContainer = modelGenerator.EntityContainer, + Namespace = modelsNamespace, + ModelsNamespace = modelsNamespace, + MappingNamespace = mappingNamespace, + EntityFrameworkVersion = entityFrameworkVersion, + TableSet = mappings.EntityMappings[entityType].Item1, + PropertyToColumnMappings = mappings.EntityMappings[entityType].Item2, + ManyToManyMappings = mappings.ManyToManyMappings + }; + var entityContents = templateProcessor.Process(Templates.EntityTemplate, entityHost); + + var filePath = Path.Combine(modelsDirectory, entityType.Name + entityHost.FileExtension); + project.AddNewFile(filePath, entityContents); + + var mappingHost = new EfTextTemplateHost + { + EntityType = entityType, + EntityContainer = modelGenerator.EntityContainer, + Namespace = mappingNamespace, + ModelsNamespace = modelsNamespace, + MappingNamespace = mappingNamespace, + EntityFrameworkVersion = entityFrameworkVersion, + TableSet = mappings.EntityMappings[entityType].Item1, + PropertyToColumnMappings = mappings.EntityMappings[entityType].Item2, + ManyToManyMappings = mappings.ManyToManyMappings + }; + var mappingContents = templateProcessor.Process(Templates.MappingTemplate, mappingHost); + + var mappingFilePath = Path.Combine(mappingDirectory, entityType.Name + "Map" + mappingHost.FileExtension); + project.AddNewFile(mappingFilePath, mappingContents); + } + + // Generate Context + _package.DTE2.StatusBar.Text = Strings.ReverseEngineer_GenerateContext; + var contextHost = new EfTextTemplateHost + { + EntityContainer = modelGenerator.EntityContainer, + Namespace = modelsNamespace, + ModelsNamespace = modelsNamespace, + MappingNamespace = mappingNamespace, + EntityFrameworkVersion = entityFrameworkVersion + }; + var contextContents = templateProcessor.Process(Templates.ContextTemplate, contextHost); + + var contextFilePath = Path.Combine(modelsDirectory, modelGenerator.EntityContainer.Name + contextHost.FileExtension); + var contextItem = project.AddNewFile(contextFilePath, contextContents); + AddConnectionStringToConfigFile(project, connectionString, providerInvariant, modelGenerator.EntityContainer.Name); + + if (contextItem != null) + { + // Open context class when done + _package.DTE2.ItemOperations.OpenFile(contextFilePath); + } + + _package.DTE2.StatusBar.Text = Strings.ReverseEngineer_Complete; + } + } + catch (Exception exception) + { + _package.LogError(Strings.ReverseEngineer_Error, exception); + } + } + + private static Version GetEntityFrameworkVersion(IEnumerable references) + { + var entityFrameworkReference = references.FirstOrDefault(r => r.Name == "EntityFramework"); + + if (entityFrameworkReference != null) + { + return new Version(entityFrameworkReference.Version); + } + + return null; + } + + private static void AddConnectionStringToConfigFile(Project project, string connectionString, string providerInvariant, string connectionStringName) + { + Contract.Requires(project != null); + Contract.Requires(!string.IsNullOrWhiteSpace(providerInvariant)); + Contract.Requires(!string.IsNullOrWhiteSpace(connectionStringName)); + + // Find App.config or Web.config + var configFilePath = Path.Combine( + project.GetProjectDir(), + project.IsWebProject() + ? "Web.config" + : "App.config"); + + // Either load up the existing file or create a blank file + var config = ConfigurationManager.OpenMappedExeConfiguration( + new ExeConfigurationFileMap { ExeConfigFilename = configFilePath }, + ConfigurationUserLevel.None); + + // Find or create the connectionStrings section + var connectionStringSettings = config.ConnectionStrings + .ConnectionStrings + .Cast() + .FirstOrDefault(css => css.Name == connectionStringName); + + if (connectionStringSettings == null) + { + connectionStringSettings = new ConnectionStringSettings + { + Name = connectionStringName + }; + + config.ConnectionStrings + .ConnectionStrings + .Add(connectionStringSettings); + } + + // Add in the new connection string + connectionStringSettings.ProviderName = providerInvariant; + connectionStringSettings.ConnectionString = FixUpConnectionString(connectionString, providerInvariant); + + project.DTE.SourceControl.CheckOutItemIfNeeded(configFilePath); + config.Save(); + + // Add any new file to the project + project.ProjectItems.AddFromFile(configFilePath); + } + + private static string FixUpConnectionString(string connectionString, string providerName) + { + Contract.Requires(!string.IsNullOrWhiteSpace(providerName)); + + if (providerName != "System.Data.SqlClient") + { + return connectionString; + } + + var builder = new SqlConnectionStringBuilder(connectionString) + { + MultipleActiveResultSets = true + }; + builder.Remove("Pooling"); + + return builder.ToString(); + } + + private class EdmMapping + { + public EdmMapping(EntityModelSchemaGenerator mcGenerator, StoreItemCollection store) + { + Contract.Requires(mcGenerator != null); + Contract.Requires(store != null); + + // Pull mapping xml out + var mappingDoc = new XmlDocument(); + var mappingXml = new StringBuilder(); + + using (var textWriter = new StringWriter(mappingXml)) + { + mcGenerator.WriteStorageMapping(new XmlTextWriter(textWriter)); + } + + mappingDoc.LoadXml(mappingXml.ToString()); + + var entitySets = mcGenerator.EntityContainer.BaseEntitySets.OfType(); + var associationSets = mcGenerator.EntityContainer.BaseEntitySets.OfType(); + var tableSets = store.GetItems().Single().BaseEntitySets.OfType(); + + this.EntityMappings = BuildEntityMappings(mappingDoc, entitySets, tableSets); + this.ManyToManyMappings = BuildManyToManyMappings(mappingDoc, associationSets, tableSets); + } + + public Dictionary>> EntityMappings { get; set; } + + public Dictionary>>> ManyToManyMappings { get; set; } + + private static Dictionary>>> BuildManyToManyMappings(XmlDocument mappingDoc, IEnumerable associationSets, IEnumerable tableSets) + { + Contract.Requires(mappingDoc != null); + Contract.Requires(associationSets != null); + Contract.Requires(tableSets != null); + + // Build mapping for each association + var mappings = new Dictionary>>>(); + var namespaceManager = new XmlNamespaceManager(mappingDoc.NameTable); + namespaceManager.AddNamespace("ef", mappingDoc.ChildNodes[0].NamespaceURI); + foreach (var associationSet in associationSets.Where(a => !a.ElementType.AssociationEndMembers.Where(e => e.RelationshipMultiplicity != RelationshipMultiplicity.Many).Any())) + { + var setMapping = mappingDoc.SelectSingleNode(string.Format("//ef:AssociationSetMapping[@Name=\"{0}\"]", associationSet.Name), namespaceManager); + var tableName = setMapping.Attributes["StoreEntitySet"].Value; + var tableSet = tableSets.Single(s => s.Name == tableName); + + var endMappings = new Dictionary>(); + foreach (var end in associationSet.AssociationSetEnds) + { + var propertyToColumnMappings = new Dictionary(); + var endMapping = setMapping.SelectSingleNode(string.Format("./ef:EndProperty[@Name=\"{0}\"]", end.Name), namespaceManager); + foreach (XmlNode fk in endMapping.ChildNodes) + { + var propertyName = fk.Attributes["Name"].Value; + var property = end.EntitySet.ElementType.Properties[propertyName]; + var columnName = fk.Attributes["ColumnName"].Value; + propertyToColumnMappings.Add(property, columnName); + } + + endMappings.Add(end.CorrespondingAssociationEndMember, propertyToColumnMappings); + } + + mappings.Add(associationSet.ElementType, Tuple.Create(tableSet, endMappings)); + } + + return mappings; + } + + private static Dictionary>> BuildEntityMappings(XmlDocument mappingDoc, IEnumerable entitySets, IEnumerable tableSets) + { + Contract.Requires(mappingDoc != null); + Contract.Requires(entitySets != null); + Contract.Requires(tableSets != null); + + // Build mapping for each type + var mappings = new Dictionary>>(); + var namespaceManager = new XmlNamespaceManager(mappingDoc.NameTable); + namespaceManager.AddNamespace("ef", mappingDoc.ChildNodes[0].NamespaceURI); + foreach (var entitySet in entitySets) + { + // Post VS2010 builds use a different structure for mapping + var setMapping = mappingDoc.ChildNodes[0].NamespaceURI == "http://schemas.microsoft.com/ado/2009/11/mapping/cs" + ? mappingDoc.SelectSingleNode(string.Format("//ef:EntitySetMapping[@Name=\"{0}\"]/ef:EntityTypeMapping/ef:MappingFragment", entitySet.Name), namespaceManager) + : mappingDoc.SelectSingleNode(string.Format("//ef:EntitySetMapping[@Name=\"{0}\"]", entitySet.Name), namespaceManager); + + var tableName = setMapping.Attributes["StoreEntitySet"].Value; + var tableSet = tableSets.Single(s => s.Name == tableName); + + var propertyMappings = new Dictionary(); + foreach (var prop in entitySet.ElementType.Properties) + { + var propMapping = setMapping.SelectSingleNode(string.Format("./ef:ScalarProperty[@Name=\"{0}\"]", prop.Name), namespaceManager); + var columnName = propMapping.Attributes["ColumnName"].Value; + var columnProp = tableSet.ElementType.Properties[columnName]; + + propertyMappings.Add(prop, columnProp); + } + + mappings.Add(entitySet.ElementType, Tuple.Create(tableSet, propertyMappings)); + } + + return mappings; + } + } + } +} diff --git a/src/PowerTools/Handlers/ViewContextHandler.cs b/src/PowerTools/Handlers/ViewContextHandler.cs new file mode 100644 index 00000000..14123813 --- /dev/null +++ b/src/PowerTools/Handlers/ViewContextHandler.cs @@ -0,0 +1,72 @@ +namespace Microsoft.DbContextPackage.Handlers +{ + using System; + using System.ComponentModel.Design; + using System.Diagnostics.Contracts; + using System.IO; + using System.Reflection; + using System.Xml; + using Microsoft.DbContextPackage.Resources; + using Microsoft.DbContextPackage.Utilities; + + internal class ViewContextHandler + { + private readonly DbContextPackage _package; + + public ViewContextHandler(DbContextPackage package) + { + Contract.Requires(package != null); + + _package = package; + } + + public void ViewContext(MenuCommand menuCommand, dynamic context, Type systemContextType) + { + Contract.Requires(menuCommand != null); + Contract.Requires(systemContextType != null); + + Type contextType = context.GetType(); + + try + { + var filePath = Path.Combine( + Path.GetTempPath(), + contextType.Name + + (menuCommand.CommandID.ID == PkgCmdIDList.cmdidViewEntityDataModel + ? FileExtensions.EntityDataModel + : FileExtensions.Xml)); + + if (File.Exists(filePath)) + { + File.SetAttributes(filePath, FileAttributes.Normal); + } + + using (var fileStream = File.Create(filePath)) + { + using (var xmlWriter = XmlWriter.Create(fileStream, new XmlWriterSettings { Indent = true })) + { + var edmxWriterType = systemContextType.Assembly.GetType("System.Data.Entity.Infrastructure.EdmxWriter"); + + if (edmxWriterType != null) + { + edmxWriterType.InvokeMember( + "WriteEdmx", + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, + null, + new object[] { context, xmlWriter }); + } + } + } + + _package.DTE2.ItemOperations.OpenFile(filePath); + + File.SetAttributes(filePath, FileAttributes.ReadOnly); + } + catch (Exception exception) + { + _package.LogError(Strings.ViewContextError(contextType.Name), exception); + } + } + } +} \ No newline at end of file diff --git a/src/PowerTools/Handlers/ViewDdlHandler.cs b/src/PowerTools/Handlers/ViewDdlHandler.cs new file mode 100644 index 00000000..d09102a3 --- /dev/null +++ b/src/PowerTools/Handlers/ViewDdlHandler.cs @@ -0,0 +1,48 @@ +namespace Microsoft.DbContextPackage.Handlers +{ + using System; + using System.Diagnostics.Contracts; + using System.IO; + using Microsoft.DbContextPackage.Resources; + using Microsoft.DbContextPackage.Utilities; + + internal class ViewDdlHandler + { + private readonly DbContextPackage _package; + + public ViewDdlHandler(DbContextPackage package) + { + Contract.Requires(package != null); + + _package = package; + } + + public void ViewDdl(dynamic context) + { + Type contextType = context.GetType(); + + try + { + var filePath = Path.Combine( + Path.GetTempPath(), + contextType.Name + FileExtensions.Sql); + + if (File.Exists(filePath)) + { + File.SetAttributes(filePath, FileAttributes.Normal); + } + + var objectContext = DbContextPackage.GetObjectContext(context); + + File.WriteAllText(filePath, objectContext.CreateDatabaseScript()); + File.SetAttributes(filePath, FileAttributes.ReadOnly); + + _package.DTE2.ItemOperations.OpenFile(filePath); + } + catch (Exception exception) + { + _package.LogError(Strings.ViewDdlError(contextType.Name), exception); + } + } + } +} \ No newline at end of file diff --git a/src/PowerTools/License.rtf b/src/PowerTools/License.rtf new file mode 100644 index 00000000..9a0bd19a Binary files /dev/null and b/src/PowerTools/License.rtf differ diff --git a/src/PowerTools/PkgCmdID.cs b/src/PowerTools/PkgCmdID.cs new file mode 100644 index 00000000..4a7c66ca --- /dev/null +++ b/src/PowerTools/PkgCmdID.cs @@ -0,0 +1,14 @@ +// PkgCmdID.cs +// MUST match PkgCmdID.h +namespace Microsoft.DbContextPackage +{ + internal static class PkgCmdIDList + { + public const uint cmdidViewEntityDataModel = 0x100; + public const uint cmdidViewEntityDataModelXml = 0x200; + public const uint cmdidPrecompileEntityDataModelViews = 0x300; + public const uint cmdidViewEntityModelDdl = 0x400; + public const uint cmdidReverseEngineerCodeFirst = 0x001; + public const uint cmdidCustomizeReverseEngineerTemplates = 0x005; + } +} \ No newline at end of file diff --git a/src/PowerTools/PowerTools.csproj b/src/PowerTools/PowerTools.csproj new file mode 100644 index 00000000..dc609117 --- /dev/null +++ b/src/PowerTools/PowerTools.csproj @@ -0,0 +1,261 @@ + + + + 11.0 + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {16CAD3A8-FCE0-4BC1-901A-16957CF24BD6} + Library + Properties + Microsoft.DbContextPackage + EFPowerTools + v4.0 + Program + $(registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\11.0@InstallDir)devenv.exe + /rootsuffix Exp + 0 + ..\Strict.ruleset + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE;CODE_ANALYSIS;CONTRACTS_FULL + prompt + 4 + True + False + True + False + False + False + False + False + False + False + False + False + False + False + False + True + False + False + False + True + False + False + False + EFPowerTools + Microsoft.DbContextPackage.Utilities.RuntimeFailureMethods + + + + + False + Full + DoNotBuild + 0 + + + pdbonly + true + bin\Release\ + TRACE;CODE_ANALYSIS;CONTRACTS_FULL + prompt + 4 + True + True + True + False + False + False + False + False + False + False + False + False + False + False + False + True + False + False + False + True + False + False + False + EFPowerTools + Microsoft.DbContextPackage.Utilities.RuntimeFailureMethods + + + + + False + Full + DoNotBuild + 0 + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + True + + + + + {80CC9F66-E7D8-4DDD-85B6-D9E6CD0E93E2} + 8 + 0 + 0 + primary + False + False + + + {1A31287A-4D7D-413E-8E32-3B374931BD89} + 8 + 0 + 0 + primary + False + False + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.tt + + + + + + + + + + + + + + + + Always + true + + + Always + true + + + Always + true + + + + + + + + Designer + + + true + VSPackage + Designer + + + + + TextTemplatingFileGenerator + Resources.cs + Microsoft.DbContextPackage + + + Designer + + + + + + + + + + + + Menus.ctmenu + + + + + + + true + + + + + + \ No newline at end of file diff --git a/src/PowerTools/Properties/AssemblyInfo.cs b/src/PowerTools/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..4ca92869 --- /dev/null +++ b/src/PowerTools/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DbContextPackage")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("DbContextPackage")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] +[assembly: NeutralResourcesLanguage("en-US")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + + + diff --git a/src/PowerTools/Properties/InternalsVisibleTo.cs b/src/PowerTools/Properties/InternalsVisibleTo.cs new file mode 100644 index 00000000..3e937e3e --- /dev/null +++ b/src/PowerTools/Properties/InternalsVisibleTo.cs @@ -0,0 +1,10 @@ +#if !INTERNALS_INVISIBLE + +using System.Runtime.CompilerServices; + +// For Moq +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] + +[assembly: InternalsVisibleTo("EFPowerTools.Test")] + +#endif \ No newline at end of file diff --git a/src/PowerTools/Properties/Resources.cs b/src/PowerTools/Properties/Resources.cs new file mode 100644 index 00000000..e5c269ee --- /dev/null +++ b/src/PowerTools/Properties/Resources.cs @@ -0,0 +1,427 @@ +// +namespace Microsoft.DbContextPackage.Resources +{ + using System; + using System.CodeDom.Compiler; + using System.Globalization; + using System.Resources; + using System.Threading; + + /// + /// Strongly-typed and parameterized string resources. + /// + [GeneratedCode("Resources.tt", "1.0.0.0")] + internal static class Strings + { + /// + /// A string like "An error occurred while adding custom templates. See the Output window for details." + /// + internal static string AddTemplatesError + { + get { return EntityRes.GetString(EntityRes.AddTemplatesError); } + } + + /// + /// A string like "The argument '{0}' cannot be null, empty or contain only white space." + /// + internal static string ArgumentIsNullOrWhitespace(object p0) + { + return EntityRes.GetString(EntityRes.ArgumentIsNullOrWhitespace, p0); + } + + /// + /// A string like "Build failed. Unable to discover a DbContext class." + /// + internal static string BuildFailed + { + get { return EntityRes.GetString(EntityRes.BuildFailed); } + } + + /// + /// A string like "An error occurred while trying to instantiate the DbContext type {0}. See the Output window for details." + /// + internal static string CreateContextFailed(object p0) + { + return EntityRes.GetString(EntityRes.CreateContextFailed, p0); + } + + /// + /// A string like "The Entity Data Model '{0}' has one or more schema errors inside the {1} section." + /// + internal static string EdmSchemaError(object p0, object p1) + { + return EntityRes.GetString(EntityRes.EdmSchemaError, p0, p1); + } + + /// + /// A string like "An error occurred while trying to load the configuration file. See the Output window for details." + /// + internal static string LoadConfigFailed + { + get { return EntityRes.GetString(EntityRes.LoadConfigFailed); } + } + + /// + /// A string like "A constructible type deriving from DbContext could not be found in the selected file." + /// + internal static string NoContext + { + get { return EntityRes.GetString(EntityRes.NoContext); } + } + + /// + /// A string like "Performing EDM view pre-compilation for: {0}. This operation may take several minutes." + /// + internal static string Optimize_Begin(object p0) + { + return EntityRes.GetString(EntityRes.Optimize_Begin, p0); + } + + /// + /// A string like "An error occurred while trying to initialize view generation for the DbContext type {0}. See the Output window for details." + /// + internal static string Optimize_ContextError(object p0) + { + return EntityRes.GetString(EntityRes.Optimize_ContextError, p0); + } + + /// + /// A string like "An error occurred while trying to initialize view generation for the Entity Data Model {0}. See the Output window for details." + /// + internal static string Optimize_EdmxError(object p0) + { + return EntityRes.GetString(EntityRes.Optimize_EdmxError, p0); + } + + /// + /// A string like "Finished EDM view pre-compilation for: {0}! See file: {1}." + /// + internal static string Optimize_End(object p0, object p1) + { + return EntityRes.GetString(EntityRes.Optimize_End, p0, p1); + } + + /// + /// A string like "An error occurred while trying to generate views for {0}. See the Output window for details." + /// + internal static string Optimize_Error(object p0) + { + return EntityRes.GetString(EntityRes.Optimize_Error, p0); + } + + /// + /// A string like "An error occurred while trying to generate views for {0}." + /// + internal static string Optimize_SchemaError(object p0) + { + return EntityRes.GetString(EntityRes.Optimize_SchemaError, p0); + } + + /// + /// A string like "The precondition '{0}' failed. {1}" + /// + internal static string PreconditionFailed(object p0, object p1) + { + return EntityRes.GetString(EntityRes.PreconditionFailed, p0, p1); + } + + /// + /// A string like "One or more errors occurred while processing template '{0}'." + /// + internal static string ProcessTemplateError(object p0) + { + return EntityRes.GetString(EntityRes.ProcessTemplateError, p0); + } + + /// + /// A string like "Reverse engineer complete." + /// + internal static string ReverseEngineer_Complete + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_Complete); } + } + + /// + /// A string like "An error occurred while reverse engineering Code First. See the Output window for details." + /// + internal static string ReverseEngineer_Error + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_Error); } + } + + /// + /// A string like "Generating entity and mapping classes..." + /// + internal static string ReverseEngineer_GenerateClasses + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_GenerateClasses); } + } + + /// + /// A string like "Generating context..." + /// + internal static string ReverseEngineer_GenerateContext + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_GenerateContext); } + } + + /// + /// A string like "Generating default mapping..." + /// + internal static string ReverseEngineer_GenerateMapping + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_GenerateMapping); } + } + + /// + /// A string like "Installing EntityFramework package..." + /// + internal static string ReverseEngineer_InstallEntityFramework + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_InstallEntityFramework); } + } + + /// + /// A string like "An error occurred while trying to install the EntityFramework package. See the Output window for details." + /// + internal static string ReverseEngineer_InstallEntityFrameworkError + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_InstallEntityFrameworkError); } + } + + /// + /// A string like "Loading schema information..." + /// + internal static string ReverseEngineer_LoadSchema + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_LoadSchema); } + } + + /// + /// A string like "One or more errors occurred while loading schema information." + /// + internal static string ReverseEngineer_SchemaError + { + get { return EntityRes.GetString(EntityRes.ReverseEngineer_SchemaError); } + } + + /// + /// A string like "Cannot find processor for directive '{0}'." + /// + internal static string UnknownDirectiveProcessor(object p0) + { + return EntityRes.GetString(EntityRes.UnknownDirectiveProcessor, p0); + } + + /// + /// A string like "You are using a version of the Entity Framework that is not supported by the Power Tools. Please upgrade to Entity Framework 4.2 or later." + /// + internal static string UnsupportedVersion + { + get { return EntityRes.GetString(EntityRes.UnsupportedVersion); } + } + + /// + /// A string like "An error occurred while trying to build the model for {0}. See the Output window for details." + /// + internal static string ViewContextError(object p0) + { + return EntityRes.GetString(EntityRes.ViewContextError, p0); + } + + /// + /// A string like "An error occurred while trying to build the model for {0}. See the Output window for details." + /// + internal static string ViewDdlError(object p0) + { + return EntityRes.GetString(EntityRes.ViewDdlError, p0); + } + } + + /// + /// Strongly-typed and parameterized exception factory. + /// + [GeneratedCode("Resources.tt", "1.0.0.0")] + internal static class Error + { + /// + /// ArgumentException with message like "The argument '{0}' cannot be null, empty or contain only white space." + /// + internal static Exception ArgumentIsNullOrWhitespace(object p0) + { + return new ArgumentException(Strings.ArgumentIsNullOrWhitespace(p0)); + } + + /// + /// ArgumentException with message like "The precondition '{0}' failed. {1}" + /// + internal static Exception PreconditionFailed(object p0, object p1) + { + return new ArgumentException(Strings.PreconditionFailed(p0, p1)); + } + + /// + /// InvalidOperationException with message like "Cannot find processor for directive '{0}'." + /// + internal static Exception UnknownDirectiveProcessor(object p0) + { + return new InvalidOperationException(Strings.UnknownDirectiveProcessor(p0)); + } + + /// + /// InvalidOperationException with message like "You are using a version of the Entity Framework that is not supported by the Power Tools. Please upgrade to Entity Framework 4.2 or later." + /// + internal static Exception UnsupportedVersion() + { + return new InvalidOperationException(Strings.UnsupportedVersion); + } + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument. + /// + internal static Exception ArgumentNull(string paramName) + { + return new ArgumentNullException(paramName); + } + + /// + /// The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method. + /// + internal static Exception ArgumentOutOfRange(string paramName) + { + return new ArgumentOutOfRangeException(paramName); + } + + /// + /// The exception that is thrown when the author has yet to implement the logic at this point in the program. This can act as an exception based TODO tag. + /// + internal static Exception NotImplemented() + { + return new NotImplementedException(); + } + + /// + /// The exception that is thrown when an invoked method is not supported, or when there is an attempt to read, seek, or write to a stream that does not support the invoked functionality. + /// + internal static Exception NotSupported() + { + return new NotSupportedException(); + } + } + + /// + /// AutoGenerated resource class. Usage: + /// + /// string s = EntityRes.GetString(EntityRes.MyIdenfitier); + /// + [GeneratedCode("Resources.tt", "1.0.0.0")] + internal sealed class EntityRes + { + internal const string AddTemplatesError = "AddTemplatesError"; + internal const string ArgumentIsNullOrWhitespace = "ArgumentIsNullOrWhitespace"; + internal const string BuildFailed = "BuildFailed"; + internal const string CreateContextFailed = "CreateContextFailed"; + internal const string EdmSchemaError = "EdmSchemaError"; + internal const string LoadConfigFailed = "LoadConfigFailed"; + internal const string NoContext = "NoContext"; + internal const string Optimize_Begin = "Optimize_Begin"; + internal const string Optimize_ContextError = "Optimize_ContextError"; + internal const string Optimize_EdmxError = "Optimize_EdmxError"; + internal const string Optimize_End = "Optimize_End"; + internal const string Optimize_Error = "Optimize_Error"; + internal const string Optimize_SchemaError = "Optimize_SchemaError"; + internal const string PreconditionFailed = "PreconditionFailed"; + internal const string ProcessTemplateError = "ProcessTemplateError"; + internal const string ReverseEngineer_Complete = "ReverseEngineer_Complete"; + internal const string ReverseEngineer_Error = "ReverseEngineer_Error"; + internal const string ReverseEngineer_GenerateClasses = "ReverseEngineer_GenerateClasses"; + internal const string ReverseEngineer_GenerateContext = "ReverseEngineer_GenerateContext"; + internal const string ReverseEngineer_GenerateMapping = "ReverseEngineer_GenerateMapping"; + internal const string ReverseEngineer_InstallEntityFramework = "ReverseEngineer_InstallEntityFramework"; + internal const string ReverseEngineer_InstallEntityFrameworkError = "ReverseEngineer_InstallEntityFrameworkError"; + internal const string ReverseEngineer_LoadSchema = "ReverseEngineer_LoadSchema"; + internal const string ReverseEngineer_SchemaError = "ReverseEngineer_SchemaError"; + internal const string UnknownDirectiveProcessor = "UnknownDirectiveProcessor"; + internal const string UnsupportedVersion = "UnsupportedVersion"; + internal const string ViewContextError = "ViewContextError"; + internal const string ViewDdlError = "ViewDdlError"; + + static EntityRes loader = null; + ResourceManager resources; + + private EntityRes() + { + resources = new ResourceManager("Microsoft.DbContextPackage.Properties.Resources", typeof(Microsoft.DbContextPackage.DbContextPackage).Assembly); + } + + private static EntityRes GetLoader() + { + if (loader == null) + { + EntityRes sr = new EntityRes(); + Interlocked.CompareExchange(ref loader, sr, null); + } + return loader; + } + + private static CultureInfo Culture + { + get { return null/*use ResourceManager default, CultureInfo.CurrentUICulture*/; } + } + + public static ResourceManager Resources + { + get + { + return GetLoader().resources; + } + } + + public static string GetString(string name, params object[] args) + { + EntityRes sys = GetLoader(); + if (sys == null) + return null; + string res = sys.resources.GetString(name, EntityRes.Culture); + + if (args != null && args.Length > 0) + { + for (int i = 0; i < args.Length; i++) + { + String value = args[i] as String; + if (value != null && value.Length > 1024) + { + args[i] = value.Substring(0, 1024 - 3) + "..."; + } + } + return String.Format(CultureInfo.CurrentCulture, res, args); + } + else + { + return res; + } + } + + public static string GetString(string name) + { + EntityRes sys = GetLoader(); + if (sys == null) + return null; + return sys.resources.GetString(name, EntityRes.Culture); + } + + public static string GetString(string name, out bool usedFallback) + { + // always false for this version of gensr + usedFallback = false; + return GetString(name); + } + + public static object GetObject(string name) + { + EntityRes sys = GetLoader(); + if (sys == null) + return null; + return sys.resources.GetObject(name, EntityRes.Culture); + } + } +} diff --git a/src/PowerTools/Properties/Resources.resx b/src/PowerTools/Properties/Resources.resx new file mode 100644 index 00000000..c8078e36 --- /dev/null +++ b/src/PowerTools/Properties/Resources.resx @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An error occurred while adding custom templates. See the Output window for details. + + + The argument '{0}' cannot be null, empty or contain only white space. + ## ExceptionType=ArgumentException + + + Build failed. Unable to discover a DbContext class. + + + An error occurred while trying to instantiate the DbContext type {0}. See the Output window for details. + + + The Entity Data Model '{0}' has one or more schema errors inside the {1} section. + + + An error occurred while trying to load the configuration file. See the Output window for details. + + + A constructible type deriving from DbContext could not be found in the selected file. + + + Performing EDM view pre-compilation for: {0}. This operation may take several minutes. + + + An error occurred while trying to initialize view generation for the DbContext type {0}. See the Output window for details. + + + An error occurred while trying to initialize view generation for the Entity Data Model {0}. See the Output window for details. + + + Finished EDM view pre-compilation for: {0}! See file: {1}. + + + An error occurred while trying to generate views for {0}. See the Output window for details. + + + An error occurred while trying to generate views for {0}. + + + The precondition '{0}' failed. {1} + ## ExceptionType=ArgumentException + + + One or more errors occurred while processing template '{0}'. + + + Reverse engineer complete. + + + An error occurred while reverse engineering Code First. See the Output window for details. + + + Generating entity and mapping classes... + + + Generating context... + + + Generating default mapping... + + + Installing EntityFramework package... + + + An error occurred while trying to install the EntityFramework package. See the Output window for details. + + + Loading schema information... + + + One or more errors occurred while loading schema information. + + + Cannot find processor for directive '{0}'. + ## ExceptionType=InvalidOperationException + + + You are using a version of the Entity Framework that is not supported by the Power Tools. Please upgrade to Entity Framework 4.2 or later. + ## ExceptionType=InvalidOperationException + + + An error occurred while trying to build the model for {0}. See the Output window for details. + + + An error occurred while trying to build the model for {0}. See the Output window for details. + + \ No newline at end of file diff --git a/src/PowerTools/Properties/Resources.tt b/src/PowerTools/Properties/Resources.tt new file mode 100644 index 00000000..32f65f8d --- /dev/null +++ b/src/PowerTools/Properties/Resources.tt @@ -0,0 +1,234 @@ +<#@ template debug="true" hostspecific="true" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ assembly name="System.Windows.Forms" #> +<#@ import namespace="System" #> +<#@ import namespace="System.Collections" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Resources" #> +<#@ import namespace="System.Text.RegularExpressions" #> +<#@ output extension=".cs" #> +<# + +var parameterMatcher = new Regex(@"\{(\d)\}"); +var lines = new List>(); + +using (var resxReader = new ResXResourceReader(Path.ChangeExtension(Host.TemplateFile, "resx"))) +{ + resxReader.UseResXDataNodes = true; + + foreach (DictionaryEntry entry in resxReader) + { + var node = (ResXDataNode)entry.Value; + var value = (string)node.GetValue((System.ComponentModel.Design.ITypeResolutionService)null); + + var matchedArgs + = parameterMatcher.Matches(value) + .Cast() + .Select(m => Convert.ToInt32(m.Groups[1].Value)) + .ToArray(); + + var argGenerator + = new object[matchedArgs.Any() ? matchedArgs.Max() + 1 : 0]; + + lines.Add(Tuple.Create( + node.Name, + value, + node.Comment.StartsWith("## ExceptionType=") ? node.Comment.Substring(17) : null, + argGenerator.Any(), + string.Join(", ", argGenerator.Select((_, i) => "p" + i)), + "(" + string.Join(", ", argGenerator.Select((_, i) => "object p" + i)) + ")" + )); + } +} + +string outputNamespace = Host.ResolveParameterValue("directiveId", "namespaceDirectiveProcessor", "namespaceHint") ?? string.Empty; +#> +// +namespace <#= outputNamespace #>.Resources +{ + using System; + using System.CodeDom.Compiler; + using System.Globalization; + using System.Resources; + using System.Threading; + + /// + /// Strongly-typed and parameterized string resources. + /// + [GeneratedCode("<#= Path.GetFileName(Host.TemplateFile) #>", "1.0.0.0")] + internal static class Strings + {<# + foreach (var line in lines) + { + #> + + /// + /// A string like "<#= line.Item2 #>" + /// + internal static string <#= line.Item1 #><#= line.Item4 ? line.Item6 : string.Empty #> + { + <# + if (!line.Item4) + { + #>get { return EntityRes.GetString(EntityRes.<#= line.Item1 #>); } +<# + } + else + { + #>return EntityRes.GetString(EntityRes.<#= line.Item1 #>, <#= line.Item5 #>); +<# + }#> + } +<# + }#> + } + + /// + /// Strongly-typed and parameterized exception factory. + /// + [GeneratedCode("<#= Path.GetFileName(Host.TemplateFile) #>", "1.0.0.0")] + internal static class Error + {<# + foreach (var line in lines.Where(l => l.Item3 != null)) + { + #> + + /// + /// <#= line.Item3 #> with message like "<#= line.Item2 #>" + /// + internal static Exception <#= line.Item1 #><#= line.Item4 ? line.Item6 : "()" #> + { + return new <#= line.Item3 #>(Strings.<#= line.Item1 #><#= line.Item4 ? "(" + line.Item5 + ")" : string.Empty #>); + } +<# + }#> + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument. + /// + internal static Exception ArgumentNull(string paramName) + { + return new ArgumentNullException(paramName); + } + + /// + /// The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method. + /// + internal static Exception ArgumentOutOfRange(string paramName) + { + return new ArgumentOutOfRangeException(paramName); + } + + /// + /// The exception that is thrown when the author has yet to implement the logic at this point in the program. This can act as an exception based TODO tag. + /// + internal static Exception NotImplemented() + { + return new NotImplementedException(); + } + + /// + /// The exception that is thrown when an invoked method is not supported, or when there is an attempt to read, seek, or write to a stream that does not support the invoked functionality. + /// + internal static Exception NotSupported() + { + return new NotSupportedException(); + } + } + + /// + /// AutoGenerated resource class. Usage: + /// + /// string s = EntityRes.GetString(EntityRes.MyIdenfitier); + /// + [GeneratedCode("<#= Path.GetFileName(Host.TemplateFile) #>", "1.0.0.0")] + internal sealed class EntityRes + { +<# + foreach (var line in lines) + {#> + internal const string <#= line.Item1 #> = "<#= line.Item1 #>"; +<# + }#> + + static EntityRes loader = null; + ResourceManager resources; + + private EntityRes() + { + resources = new ResourceManager("Microsoft.DbContextPackage.Properties.<#= Path.GetFileNameWithoutExtension(Host.TemplateFile) #>", typeof(Microsoft.DbContextPackage.DbContextPackage).Assembly); + } + + private static EntityRes GetLoader() + { + if (loader == null) + { + EntityRes sr = new EntityRes(); + Interlocked.CompareExchange(ref loader, sr, null); + } + return loader; + } + + private static CultureInfo Culture + { + get { return null/*use ResourceManager default, CultureInfo.CurrentUICulture*/; } + } + + public static ResourceManager Resources + { + get + { + return GetLoader().resources; + } + } + + public static string GetString(string name, params object[] args) + { + EntityRes sys = GetLoader(); + if (sys == null) + return null; + string res = sys.resources.GetString(name, EntityRes.Culture); + + if (args != null && args.Length > 0) + { + for (int i = 0; i < args.Length; i++) + { + String value = args[i] as String; + if (value != null && value.Length > 1024) + { + args[i] = value.Substring(0, 1024 - 3) + "..."; + } + } + return String.Format(CultureInfo.CurrentCulture, res, args); + } + else + { + return res; + } + } + + public static string GetString(string name) + { + EntityRes sys = GetLoader(); + if (sys == null) + return null; + return sys.resources.GetString(name, EntityRes.Culture); + } + + public static string GetString(string name, out bool usedFallback) + { + // always false for this version of gensr + usedFallback = false; + return GetString(name); + } + + public static object GetObject(string name) + { + EntityRes sys = GetLoader(); + if (sys == null) + return null; + return sys.resources.GetObject(name, EntityRes.Culture); + } + } +} diff --git a/src/PowerTools/Resources/1.png b/src/PowerTools/Resources/1.png new file mode 100644 index 00000000..5e815a7c Binary files /dev/null and b/src/PowerTools/Resources/1.png differ diff --git a/src/PowerTools/Resources/2.png b/src/PowerTools/Resources/2.png new file mode 100644 index 00000000..59e941d3 Binary files /dev/null and b/src/PowerTools/Resources/2.png differ diff --git a/src/PowerTools/Resources/3.png b/src/PowerTools/Resources/3.png new file mode 100644 index 00000000..1ff480d4 Binary files /dev/null and b/src/PowerTools/Resources/3.png differ diff --git a/src/PowerTools/Resources/4.png b/src/PowerTools/Resources/4.png new file mode 100644 index 00000000..3daedfe0 Binary files /dev/null and b/src/PowerTools/Resources/4.png differ diff --git a/src/PowerTools/Resources/5.png b/src/PowerTools/Resources/5.png new file mode 100644 index 00000000..04cc9e72 Binary files /dev/null and b/src/PowerTools/Resources/5.png differ diff --git a/src/PowerTools/Resources/Package.ico b/src/PowerTools/Resources/Package.ico new file mode 100644 index 00000000..ea3b23fe Binary files /dev/null and b/src/PowerTools/Resources/Package.ico differ diff --git a/src/PowerTools/Utilities/EdmxUtility.cs b/src/PowerTools/Utilities/EdmxUtility.cs new file mode 100644 index 00000000..006c5457 --- /dev/null +++ b/src/PowerTools/Utilities/EdmxUtility.cs @@ -0,0 +1,148 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System.Collections.Generic; + using System.Data.Entity.Design; + using System.Data.Mapping; + using System.Data.Metadata.Edm; + using System.Diagnostics.Contracts; + using System.IO; + using System.Xml; + using System.Xml.Linq; + using Microsoft.DbContextPackage.Extensions; + using Microsoft.DbContextPackage.Resources; + + internal class EdmxUtility + { + private static readonly IEnumerable EDMX_NAMESPACES = new XNamespace[] + { + "http://schemas.microsoft.com/ado/2009/11/edmx", + "http://schemas.microsoft.com/ado/2008/10/edmx", + "http://schemas.microsoft.com/ado/2007/06/edmx" + }; + + private readonly string _edmxPath; + + public EdmxUtility(string edmxPath) + { + Contract.Requires(!string.IsNullOrWhiteSpace(edmxPath)); + + _edmxPath = edmxPath; + } + + public StorageMappingItemCollection GetMappingCollection() + { + IList errors; + var edmxFileName = Path.GetFileName(_edmxPath); + + EdmItemCollection edmCollection; + using (var reader = CreateSectionReader(EdmxSection.Csdl)) + { + edmCollection = MetadataItemCollectionFactory.CreateEdmItemCollection( + new[] { reader }, + out errors); + errors.HandleErrors(Strings.EdmSchemaError(edmxFileName, EdmxSection.Csdl.SectionName)); + } + + StoreItemCollection storeCollection; + using (var reader = CreateSectionReader(EdmxSection.Ssdl)) + { + storeCollection = MetadataItemCollectionFactory.CreateStoreItemCollection( + new[] { reader }, + out errors); + errors.HandleErrors(Strings.EdmSchemaError(edmxFileName, EdmxSection.Ssdl.SectionName)); + } + + StorageMappingItemCollection mappingCollection; + using (var reader = CreateSectionReader(EdmxSection.Msl)) + { + mappingCollection = MetadataItemCollectionFactory.CreateStorageMappingItemCollection( + edmCollection, + storeCollection, + new[] { reader }, + out errors); + errors.HandleErrors(Strings.EdmSchemaError(edmxFileName, EdmxSection.Msl.SectionName)); + } + + return mappingCollection; + } + + private XmlReader CreateSectionReader(EdmxSection edmxSection) + { + Contract.Requires(edmxSection != null); + + var edmxDocument = XElement.Load(_edmxPath, LoadOptions.SetBaseUri | LoadOptions.SetLineInfo); + + var runtime = edmxDocument.Element(EDMX_NAMESPACES, "Runtime"); + if (runtime == null) + { + return null; + } + + var section = runtime.Element(EDMX_NAMESPACES, edmxSection.SectionName); + if (section == null) + { + return null; + } + + var rootElement = section.Element(edmxSection.Namespaces, edmxSection.RootElementName); + if (rootElement == null) + { + return null; + } + + return rootElement.CreateReader(); + } + + private sealed class EdmxSection + { + static EdmxSection() + { + Csdl = new EdmxSection + { + Namespaces = new XNamespace[] + { + "http://schemas.microsoft.com/ado/2009/11/edm", + "http://schemas.microsoft.com/ado/2008/09/edm", + "http://schemas.microsoft.com/ado/2006/04/edm" + }, + SectionName = "ConceptualModels", + RootElementName = "Schema" + }; + Msl = new EdmxSection + { + Namespaces = new XNamespace[] + { + "http://schemas.microsoft.com/ado/2009/11/mapping/cs", + "http://schemas.microsoft.com/ado/2008/09/mapping/cs", + "urn:schemas-microsoft-com:windows:storage:mapping:CS" + }, + SectionName = "Mappings", + RootElementName = "Mapping" + }; + Ssdl = new EdmxSection + { + Namespaces = new XNamespace[] + { + "http://schemas.microsoft.com/ado/2009/11/edm/ssdl", + "http://schemas.microsoft.com/ado/2009/02/edm/ssdl", + "http://schemas.microsoft.com/ado/2006/04/edm/ssdl" + }, + SectionName = "StorageModels", + RootElementName = "Schema" + }; + } + + private EdmxSection() + { + } + + public static EdmxSection Csdl { get; private set; } + public static EdmxSection Msl { get; private set; } + public static EdmxSection Ssdl { get; private set; } + + public IEnumerable Namespaces { get; private set; } + public string SectionName { get; private set; } + public string RootElementName { get; private set; } + } + } +} diff --git a/src/PowerTools/Utilities/EfTextTemplateHost.cs b/src/PowerTools/Utilities/EfTextTemplateHost.cs new file mode 100644 index 00000000..a870ef90 --- /dev/null +++ b/src/PowerTools/Utilities/EfTextTemplateHost.cs @@ -0,0 +1,192 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.Data.Metadata.Edm; + using System.Data.Objects; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using Microsoft.DbContextPackage.Resources; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.Shell.Interop; + using Microsoft.VisualStudio.TextTemplating; + + public class EfTextTemplateHost : ITextTemplatingEngineHost + { + public EntityType EntityType { get; set; } + public EntityContainer EntityContainer { get; set; } + public string Namespace { get; set; } + public string ModelsNamespace { get; set; } + public string MappingNamespace { get; set; } + public Version EntityFrameworkVersion { get; set; } + public EntitySet TableSet { get; set; } + public Dictionary PropertyToColumnMappings { get; set; } + public Dictionary>>> ManyToManyMappings { get; set; } + + #region T4 plumbing + + public CompilerErrorCollection Errors { get; set; } + public string FileExtension { get; set; } + public Encoding OutputEncoding { get; set; } + public string TemplateFile { get; set; } + + public virtual string ResolveAssemblyReference(string assemblyReference) + { + if (File.Exists(assemblyReference)) + { + return assemblyReference; + } + + try + { + // TODO: This is failing to resolve partial assembly names (e.g. "System.Xml") + var assembly = Assembly.Load(assemblyReference); + + if (assembly != null) + { + return assembly.Location; + } + } + catch (FileNotFoundException) + { + } + catch (FileLoadException) + { + } + catch (BadImageFormatException) + { + } + + return string.Empty; + } + + IList ITextTemplatingEngineHost.StandardAssemblyReferences + { + get + { + return new[] + { + Assembly.GetExecutingAssembly().Location, + typeof(Uri).Assembly.Location, + typeof(Enumerable).Assembly.Location, + typeof(ObjectContext).Assembly.Location, + + // HACK: Because of the issue in ResolveAssemblyReference, these are not being + // loaded but are required by the default templates + typeof(System.Data.AcceptRejectRule).Assembly.Location, + typeof(System.Data.Entity.Design.EdmToObjectNamespaceMap).Assembly.Location, + typeof(System.Xml.ConformanceLevel).Assembly.Location, + typeof(System.Xml.Linq.Extensions).Assembly.Location, + typeof(EnvDTE._BuildEvents).Assembly.Location + }; + } + } + + IList ITextTemplatingEngineHost.StandardImports + { + get + { + return new[] + { + "System", + "Microsoft.DbContextPackage.Utilities" + }; + } + } + + object ITextTemplatingEngineHost.GetHostOption(string optionName) + { + if (optionName == "CacheAssemblies") + { + return 1; + } + + return null; + } + + bool ITextTemplatingEngineHost.LoadIncludeText(string requestFileName, out string content, out string location) + { + location = ((ITextTemplatingEngineHost)this).ResolvePath(requestFileName); + + if (File.Exists(location)) + { + content = File.ReadAllText(location); + + return true; + } + + using (var rootKey = VSRegistry.RegistryRoot(__VsLocalRegistryType.RegType_Configuration)) + using (var includeFoldersKey = rootKey.OpenSubKey(@"TextTemplating\IncludeFolders\.tt")) + { + foreach (var valueName in includeFoldersKey.GetValueNames()) + { + var includeFolder = includeFoldersKey.GetValue(valueName) as string; + + if (includeFolder == null) + { + continue; + } + + location = Path.Combine(includeFolder, requestFileName); + + if (File.Exists(location)) + { + content = File.ReadAllText(location); + + return true; + } + } + } + + location = string.Empty; + content = string.Empty; + + return false; + } + + void ITextTemplatingEngineHost.LogErrors(CompilerErrorCollection errors) + { + Errors = errors; + } + + AppDomain ITextTemplatingEngineHost.ProvideTemplatingAppDomain(string content) + { + return AppDomain.CurrentDomain; + } + + Type ITextTemplatingEngineHost.ResolveDirectiveProcessor(string processorName) + { + throw Error.UnknownDirectiveProcessor(processorName); + } + + string ITextTemplatingEngineHost.ResolveParameterValue(string directiveId, string processorName, string parameterName) + { + return string.Empty; + } + + string ITextTemplatingEngineHost.ResolvePath(string path) + { + if (!Path.IsPathRooted(path) && Path.IsPathRooted(TemplateFile)) + { + return Path.Combine(Path.GetDirectoryName(TemplateFile), path); + } + + return path; + } + + void ITextTemplatingEngineHost.SetFileExtension(string extension) + { + FileExtension = extension; + } + + void ITextTemplatingEngineHost.SetOutputEncoding(Encoding encoding, bool fromOutputDirective) + { + OutputEncoding = encoding; + } + + #endregion + } +} diff --git a/src/PowerTools/Utilities/FileExtensions.cs b/src/PowerTools/Utilities/FileExtensions.cs new file mode 100644 index 00000000..e0a66094 --- /dev/null +++ b/src/PowerTools/Utilities/FileExtensions.cs @@ -0,0 +1,11 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + internal static class FileExtensions + { + public const string CSharp = ".cs"; + public const string VisualBasic = ".vb"; + public const string EntityDataModel = ".edmx"; + public const string Xml = ".xml"; + public const string Sql = ".sql"; + } +} diff --git a/src/PowerTools/Utilities/RuntimeFailureMethods.cs b/src/PowerTools/Utilities/RuntimeFailureMethods.cs new file mode 100644 index 00000000..f2ae472b --- /dev/null +++ b/src/PowerTools/Utilities/RuntimeFailureMethods.cs @@ -0,0 +1,44 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System.Diagnostics; + using System.Text.RegularExpressions; + using Microsoft.DbContextPackage.Resources; + + /// + /// Code Contracts hook methods - Called when contracts fail. Here we detect the + /// most common preconditions so we can throw the correct exceptions. It also + /// means that we can write preconditions using the simplest Contract.Requires() + /// form. + /// + internal static class RuntimeFailureMethods + { + public static readonly Regex IsNotNull = new Regex( + @"^\s*(@?\w+)\s*\!\=\s*null\s*$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex IsNullOrWhiteSpace = new Regex( + @"^\s*\!\s*string\s*\.\s*IsNullOrWhiteSpace\s*\(\s*(@?[\w]+)\s*\)\s*$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + [DebuggerStepThrough] + public static void Requires(bool condition, string userMessage, string conditionText) + { + if (!condition) + { + Match match; + + if (((match = IsNotNull.Match(conditionText)) != null) && match.Success) + { + throw Error.ArgumentNull(match.Groups[1].Value); + } + + if (((match = IsNullOrWhiteSpace.Match(conditionText)) != null) && match.Success) + { + throw Error.ArgumentIsNullOrWhitespace(match.Groups[1].Value); + } + + throw Error.PreconditionFailed(conditionText, userMessage); + } + } + } +} \ No newline at end of file diff --git a/src/PowerTools/Utilities/TemplateProcessor.cs b/src/PowerTools/Utilities/TemplateProcessor.cs new file mode 100644 index 00000000..ce85fbc5 --- /dev/null +++ b/src/PowerTools/Utilities/TemplateProcessor.cs @@ -0,0 +1,94 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.IO; + using EnvDTE; + using Microsoft.DbContextPackage.Extensions; + using Microsoft.DbContextPackage.Resources; + using Microsoft.VisualStudio.Shell; + using Microsoft.VisualStudio.TextTemplating; + using Microsoft.VisualStudio.TextTemplating.VSHost; + + internal class TemplateProcessor + { + private readonly Project _project; + private readonly IDictionary _templateCache; + + public TemplateProcessor(Project project) + { + Contract.Requires(project != null); + + _project = project; + _templateCache = new Dictionary(); + } + + public string Process(string templatePath, EfTextTemplateHost host) + { + Contract.Requires(!string.IsNullOrWhiteSpace(templatePath)); + Contract.Requires(host != null); + + host.TemplateFile = templatePath; + + var output = GetEngine().ProcessTemplate( + GetTemplate(templatePath), + host); + + host.Errors.HandleErrors(Strings.ProcessTemplateError(Path.GetFileName(templatePath))); + + return output; + } + + private string GetTemplate(string templatePath) + { + Contract.Requires(!string.IsNullOrWhiteSpace(templatePath)); + + if (_templateCache.ContainsKey(templatePath)) + { + return _templateCache[templatePath]; + } + + var items = templatePath.Split('\\'); + Contract.Assert(items.Length > 1); + + var childProjectItem + = _project.ProjectItems + .GetItem(items[0]); + + for (int i = 1; childProjectItem != null && i < items.Length; i++) + { + var item = items[i]; + + childProjectItem = childProjectItem.ProjectItems.GetItem(item); + } + + string contents = null; + + if (childProjectItem != null) + { + var path = (string)childProjectItem.Properties.Item("FullPath").Value; + + if (!string.IsNullOrWhiteSpace(path)) + { + contents = File.ReadAllText(path); + } + } + + if (contents == null) + { + contents = Templates.GetDefaultTemplate(templatePath); + } + + _templateCache.Add(templatePath, contents); + + return contents; + } + + private static ITextTemplatingEngine GetEngine() + { + var textTemplating = (ITextTemplatingComponents)Package.GetGlobalService(typeof(STextTemplating)); + + return textTemplating.Engine; + } + } +} diff --git a/src/PowerTools/Utilities/Templates.cs b/src/PowerTools/Utilities/Templates.cs new file mode 100644 index 00000000..25d4772d --- /dev/null +++ b/src/PowerTools/Utilities/Templates.cs @@ -0,0 +1,27 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System.Diagnostics.Contracts; + using System.IO; + using System.Text; + + internal static class Templates + { + public const string ContextTemplate = @"CodeTemplates\ReverseEngineerCodeFirst\Context.tt"; + public const string EntityTemplate = @"CodeTemplates\ReverseEngineerCodeFirst\Entity.tt"; + public const string MappingTemplate = @"CodeTemplates\ReverseEngineerCodeFirst\Mapping.tt"; + + public static string GetDefaultTemplate(string path) + { + Contract.Requires(!string.IsNullOrWhiteSpace(path)); + + var stream = typeof(Templates).Assembly.GetManifestResourceStream( + "Microsoft.DbContextPackage." + path.Replace('\\', '.')); + Contract.Assert(stream != null); + + using (var reader = new StreamReader(stream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } +} diff --git a/src/PowerTools/VSPackage.resx b/src/PowerTools/VSPackage.resx new file mode 100644 index 00000000..1b25371f --- /dev/null +++ b/src/PowerTools/VSPackage.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Entity Framework Power Tools + + + Adds useful design-time DbContext features to the Visual Studio Solution Explorer context menu. + +When right-clicking on a file containing a derived DbContext class, the following context menu functions are supported: + +1) View Entity Data Model - Displays the underlying Code First model in the Entity Framework designer. +2) View Entity Data Model XML - Displays the EDMX XML representing the underlying Code First model. +3) Generate Views - Generates pre-compiled views used by the EF runtime to improve start-up performance. Adds the generated views file to the containing project. + + + + Resources\Package.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/src/PowerTools/db.png b/src/PowerTools/db.png new file mode 100644 index 00000000..7ab63b04 Binary files /dev/null and b/src/PowerTools/db.png differ diff --git a/src/PowerTools/menu.png b/src/PowerTools/menu.png new file mode 100644 index 00000000..d6702f69 Binary files /dev/null and b/src/PowerTools/menu.png differ diff --git a/src/PowerTools/source.extension.vsixmanifest b/src/PowerTools/source.extension.vsixmanifest new file mode 100644 index 00000000..ad8cc973 --- /dev/null +++ b/src/PowerTools/source.extension.vsixmanifest @@ -0,0 +1,42 @@ + + + + Entity Framework Power Tools Beta 2 + Microsoft + 0.6.0.0 + Preview of useful design-time features for DbContext. + +When right-clicking on a C# project, the following context menu function is supported: +1) Reverse Engineer Code First - Generates POCO classes, derived DbContext and Code First mapping for an existing database. + +When right-clicking on a file containing a derived DbContext class, the following context menu functions are supported: + +1) View Entity Data Model XML - Displays the EDMX XML representing the underlying Code First model. +2) View Entity Data Model DDL SQL - Displays the DDL SQL corresponding to the SSDL in the underlying EDM Model. +3) Generate Views - Generates pre-compiled views used by the EF runtime to improve start-up performance. Adds the generated views file to the containing project. + 1033 + http://blogs.msdn.com/b/adonet + License.rtf + http://blogs.msdn.com/b/adonet + db.png + menu.png + false + + + Pro + + + Pro + + + + + + + Visual Studio MPF + + + + |%CurrentProject%;PkgdefProjectOutputGroup| + + diff --git a/test/PowerTools.Test/Extensions/CompilerErrorCollectionExtensionsTests.cs b/test/PowerTools.Test/Extensions/CompilerErrorCollectionExtensionsTests.cs new file mode 100644 index 00000000..b1005e92 --- /dev/null +++ b/test/PowerTools.Test/Extensions/CompilerErrorCollectionExtensionsTests.cs @@ -0,0 +1,36 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using Xunit; + using System.CodeDom.Compiler; + using System.Linq; + + public class CompilerErrorCollectionExtensionsTests + { + [Fact] + public void HandleErrors_is_noop_when_no_errors() + { + var errors = new CompilerErrorCollection + { + new CompilerError { IsWarning = true } + }; + + errors.HandleErrors("Not used"); + } + + [Fact] + public void HandleErrors_throws_when_errors() + { + var error = new CompilerError { IsWarning = false }; + var errors = new CompilerErrorCollection { error }; + var message = "Some message"; + + var ex = Assert.Throws( + () => errors.HandleErrors(message)); + + Assert.Equal(message, ex.Message); + Assert.NotNull(ex.Errors); + Assert.Equal(1, ex.Errors.Count()); + Assert.Same(error, ex.Errors.Single()); + } + } +} diff --git a/test/PowerTools.Test/Extensions/CompilerErrorExceptionTests.cs b/test/PowerTools.Test/Extensions/CompilerErrorExceptionTests.cs new file mode 100644 index 00000000..afa411fb --- /dev/null +++ b/test/PowerTools.Test/Extensions/CompilerErrorExceptionTests.cs @@ -0,0 +1,54 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + using System.CodeDom.Compiler; + using System.Collections.Generic; + using System.IO; + using System.Runtime.Serialization.Formatters.Binary; + using Xunit; + + public class CompilerErrorExceptionTests + { + [Fact] + public void Ctor_validates_preconditions() + { + IEnumerable errors = null; + + var ex = Assert.Throws( + () => new CompilerErrorException("Not used", errors)); + + Assert.Equal("errors", ex.ParamName); + } + + [Fact] + public void Ctor_sets_properties() + { + var message = "Some message"; + var errors = new CompilerError[0]; + + var ex = new CompilerErrorException(message, errors); + + Assert.Equal(message, ex.Message); + Assert.Same(errors, ex.Errors); + } + + [Fact] + public void Is_serializable() + { + var message = "Some message"; + var errors = new CompilerError[0]; + var formatter = new BinaryFormatter(); + CompilerErrorException ex; + + using (var stream = new MemoryStream()) + { + formatter.Serialize(stream, new CompilerErrorException(message, errors)); + stream.Position = 0; + ex = (CompilerErrorException)formatter.Deserialize(stream); + } + + Assert.Equal(message, ex.Message); + Assert.Equal(errors, ex.Errors); + } + } +} diff --git a/test/PowerTools.Test/Extensions/EdmSchemaErrorExceptionTests.cs b/test/PowerTools.Test/Extensions/EdmSchemaErrorExceptionTests.cs new file mode 100644 index 00000000..5bfeb9bd --- /dev/null +++ b/test/PowerTools.Test/Extensions/EdmSchemaErrorExceptionTests.cs @@ -0,0 +1,54 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System; + using System.Collections.Generic; + using System.Data.Metadata.Edm; + using System.IO; + using System.Runtime.Serialization.Formatters.Binary; + using Xunit; + + public class EdmSchemaErrorExceptionTests + { + [Fact] + public void Ctor_validates_preconditions() + { + IEnumerable errors = null; + + var ex = Assert.Throws( + () => new EdmSchemaErrorException("Not used", errors)); + + Assert.Equal("errors", ex.ParamName); + } + + [Fact] + public void Ctor_sets_properties() + { + var message = "Some message"; + var errors = new EdmSchemaError[0]; + + var ex = new EdmSchemaErrorException(message, errors); + + Assert.Equal(message, ex.Message); + Assert.Same(errors, ex.Errors); + } + + [Fact] + public void Is_serializable() + { + var message = "Some message"; + var errors = new EdmSchemaError[0]; + var formatter = new BinaryFormatter(); + EdmSchemaErrorException ex; + + using (var stream = new MemoryStream()) + { + formatter.Serialize(stream, new EdmSchemaErrorException(message, errors)); + stream.Position = 0; + ex = (EdmSchemaErrorException)formatter.Deserialize(stream); + } + + Assert.Equal(message, ex.Message); + Assert.Equal(errors, ex.Errors); + } + } +} diff --git a/test/PowerTools.Test/Extensions/IComponentModelExtensionsTests.cs b/test/PowerTools.Test/Extensions/IComponentModelExtensionsTests.cs new file mode 100644 index 00000000..a5a14b34 --- /dev/null +++ b/test/PowerTools.Test/Extensions/IComponentModelExtensionsTests.cs @@ -0,0 +1,20 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using Microsoft.VisualStudio.ComponentModelHost; + using Moq; + using Xunit; + + public class IComponentModelExtensionsTests + { + [Fact] + public void GetService_calls_generic_version() + { + var componentModelMock = new Mock(); + var componentModel = componentModelMock.Object; + + componentModel.GetService(typeof(string)); + + componentModelMock.Verify(cm => cm.GetService(), Times.Once()); + } + } +} diff --git a/test/PowerTools.Test/Extensions/StringExtensionsTests.cs b/test/PowerTools.Test/Extensions/StringExtensionsTests.cs new file mode 100644 index 00000000..93c0bf4e --- /dev/null +++ b/test/PowerTools.Test/Extensions/StringExtensionsTests.cs @@ -0,0 +1,17 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using Xunit; + + public class StringExtensionsTests + { + [Fact] + public void EqualsIgnoreCase_tests_equality() + { + Assert.True(StringExtensions.EqualsIgnoreCase(null, null)); + Assert.True("one".EqualsIgnoreCase("one")); + Assert.True("two".EqualsIgnoreCase("TWO")); + Assert.False("three".EqualsIgnoreCase("four")); + Assert.False("five".EqualsIgnoreCase(null)); + } + } +} diff --git a/test/PowerTools.Test/Extensions/XContainerExtensionsTests.cs b/test/PowerTools.Test/Extensions/XContainerExtensionsTests.cs new file mode 100644 index 00000000..903db6dd --- /dev/null +++ b/test/PowerTools.Test/Extensions/XContainerExtensionsTests.cs @@ -0,0 +1,46 @@ +namespace Microsoft.DbContextPackage.Extensions +{ + using System.Xml.Linq; + using Xunit; + + public class XContainerExtensionsTests + { + private const string _elementName = "Element"; + private readonly XNamespace _ns1 = "http://tempuri.org/1"; + private readonly XNamespace _ns2 = "http://tempuri.org/2"; + private readonly XElement _element1; + private readonly XContainer _container; + + public XContainerExtensionsTests() + { + _element1 = new XElement(_ns1 + _elementName); + _container = new XElement( + "Container", + new XElement(_elementName), + _element1, + new XElement(_ns2 + _elementName)); + } + + [Fact] + public void Element_returns_first_match() + { + var element = _container.Element( + new[] { _ns1, _ns2 }, + _elementName); + + Assert.Same(_element1, element); + } + + [Fact] + public void Element_returns_null_when_no_match() + { + XNamespace ns3 = "http://tempuri.org/3"; + + var element = _container.Element( + new[] { ns3 }, + _elementName); + + Assert.Null(element); + } + } +} diff --git a/test/PowerTools.Test/PowerTools.Test.csproj b/test/PowerTools.Test/PowerTools.Test.csproj new file mode 100644 index 00000000..15037838 --- /dev/null +++ b/test/PowerTools.Test/PowerTools.Test.csproj @@ -0,0 +1,83 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {ADCD3A3D-2564-48F3-81A9-B0B532675808} + Library + Properties + Microsoft.DbContextPackage + EFPowerTools.Test + v4.0 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + ..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll + + + ..\..\packages\xunit.1.9.0.1566\lib\xunit.dll + + + + + {16CAD3A8-FCE0-4BC1-901A-16957CF24BD6} + PowerTools + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/PowerTools.Test/Properties/AssemblyInfo.cs b/test/PowerTools.Test/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..dc9c6cb2 --- /dev/null +++ b/test/PowerTools.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PowerTools.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PowerTools.Test")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7d655301-2f25-4ca7-a8a8-82e3d02921e2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/PowerTools.Test/Utilities/EdmxUtilityTests.cs b/test/PowerTools.Test/Utilities/EdmxUtilityTests.cs new file mode 100644 index 00000000..96c5b8a6 --- /dev/null +++ b/test/PowerTools.Test/Utilities/EdmxUtilityTests.cs @@ -0,0 +1,210 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System; + using System.Collections.Generic; + using System.Data.Mapping; + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.DbContextPackage.Extensions; + using Microsoft.DbContextPackage.Resources; + using Xunit; + + public class EdmxUtilityTests + { + [Fact] + public void GetMappingCollection_returns_mapping() + { + var edmxContents = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + StorageMappingItemCollection mappingCollection; + + using (var edmx = new TempFile(edmxContents)) + { + mappingCollection = new EdmxUtility(edmx.FileName) + .GetMappingCollection(); + } + + Assert.True(mappingCollection.Contains("DatabaseEntities")); + } + + [Fact] + public void GetMappingCollection_returns_mapping_for_v2_schema() + { + var edmxContents = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + StorageMappingItemCollection mappingCollection; + + using (var edmx = new TempFile(edmxContents)) + { + mappingCollection = new EdmxUtility(edmx.FileName) + .GetMappingCollection(); + } + + Assert.True(mappingCollection.Contains("DatabaseEntities")); + } + + [Fact] + public void GetMappingCollection_throws_on_schema_errors() + { + var edmxContents = @" + + + + + + +"; + + using (var edmx = new TempFile(edmxContents)) + { + var ex = Assert.Throws( + () => new EdmxUtility(edmx.FileName).GetMappingCollection()); + + Assert.Equal( + Strings.EdmSchemaError( + Path.GetFileName(edmx.FileName), + "ConceptualModels"), + ex.Message); + } + } + + private sealed class TempFile : IDisposable + { + private readonly string _fileName; + private bool _disposed; + + public TempFile(string contents) + { + _fileName = Path.GetTempFileName(); + File.WriteAllText(_fileName, contents); + } + + ~TempFile() + { + Dispose(false); + } + + public string FileName + { + get + { + HandleDisposed(); + + return _fileName; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (!string.IsNullOrWhiteSpace(_fileName)) + { + File.Delete(_fileName); + } + + _disposed = true; + } + } + + private void HandleDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(null); + } + } + } + } +} diff --git a/test/PowerTools.Test/Utilities/EfTextTemplateHostTests.cs b/test/PowerTools.Test/Utilities/EfTextTemplateHostTests.cs new file mode 100644 index 00000000..f0b7e53a --- /dev/null +++ b/test/PowerTools.Test/Utilities/EfTextTemplateHostTests.cs @@ -0,0 +1,149 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System; + using System.CodeDom.Compiler; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Runtime.Serialization.Formatters.Binary; + using Microsoft.VisualStudio.TextTemplating; + using Xunit; + + public class EfTextTemplateHostTests + { + public class ResolveAssemblyReference + { + [Fact] + public void Resolves_assembly_locations() + { + var host = new EfTextTemplateHost(); + var assemblyLocation = typeof(Type).Assembly.Location; + + var resolvedReference = host.ResolveAssemblyReference( + assemblyLocation); + + Assert.True(File.Exists(resolvedReference)); + Assert.Equal("mscorlib.dll", Path.GetFileName(resolvedReference)); + } + + [Fact] + public void Resolves_full_assembly_names() + { + var host = new EfTextTemplateHost(); + var assemblyName = typeof(Type).Assembly.GetName(); + + var resolvedReference = host.ResolveAssemblyReference( + assemblyName.FullName); + + Assert.True(File.Exists(resolvedReference)); + Assert.Equal("mscorlib.dll", Path.GetFileName(resolvedReference)); + } + + [Fact] + public void Resolves_simple_assembly_names() + { + var host = new EfTextTemplateHost(); + var assemblyName = typeof(Type).Assembly.GetName(); + + var resolvedReference = host.ResolveAssemblyReference( + assemblyName.Name); + + Assert.True(File.Exists(resolvedReference)); + Assert.Equal("mscorlib.dll", Path.GetFileName(resolvedReference)); + } + } + + [Fact] + public void StandardAssemblyReferences_includes_basic_references() + { + ITextTemplatingEngineHost host = new EfTextTemplateHost(); + var powerToolsAssemblyName = typeof(EfTextTemplateHost) + .Assembly + .GetName() + .Name; + + var references = host.StandardAssemblyReferences + .Select(r => Path.GetFileNameWithoutExtension(r)) + .ToArray(); + + Assert.Contains(powerToolsAssemblyName, references); + Assert.Contains("System", references); + Assert.Contains("System.Core", references); + Assert.Contains("System.Data.Entity", references); + } + + [Fact] + public void StandardImports_includes_basic_imports() + { + ITextTemplatingEngineHost host = new EfTextTemplateHost(); + var hostNamespace = typeof(EfTextTemplateHost).Namespace; + + var imports = host.StandardImports; + + Assert.Contains("System", imports); + Assert.Contains(hostNamespace, imports); + } + + [Fact] + public void LogErrors_sets_Errors() + { + var efHost = new EfTextTemplateHost(); + var host = (ITextTemplatingEngineHost)efHost; + var errors = new CompilerErrorCollection(); + + host.LogErrors(errors); + + Assert.Same(errors, efHost.Errors); + } + + public class ResolvePath + { + [Fact] + public void Resolves_absolute_paths() + { + const string path = @"C:\File.ext"; + ITextTemplatingEngineHost host = new EfTextTemplateHost(); + + var resolvedPath = host.ResolvePath(path); + + Assert.Equal(path, resolvedPath); + } + + [Fact] + public void Resolves_relative_paths_when_TemplateFile_absolute() + { + ITextTemplatingEngineHost host = new EfTextTemplateHost + { + TemplateFile = @"C:\Template.tt" + }; + + var resolvedPath = host.ResolvePath("File.ext"); + + Assert.Equal(@"C:\File.ext", resolvedPath); + } + + [Fact] + public void Returns_original_path_when_unresolvable() + { + const string path = "File.ext"; + ITextTemplatingEngineHost host = new EfTextTemplateHost(); + + var resolvedPath = host.ResolvePath(path); + + Assert.Equal(path, resolvedPath); + } + } + + [Fact] + public void SetFileExtension_sets_FileExtension() + { + const string extension = ".ext"; + var efHost = new EfTextTemplateHost(); + var host = (ITextTemplatingEngineHost)efHost; + + host.SetFileExtension(extension); + + Assert.Equal(extension, efHost.FileExtension); + } + } +} diff --git a/test/PowerTools.Test/Utilities/TemplatesTests.cs b/test/PowerTools.Test/Utilities/TemplatesTests.cs new file mode 100644 index 00000000..bbc889ca --- /dev/null +++ b/test/PowerTools.Test/Utilities/TemplatesTests.cs @@ -0,0 +1,21 @@ +namespace Microsoft.DbContextPackage.Utilities +{ + using System; + using Xunit; + + public class TemplatesTests + { + [Fact] + public void GetDefaultTemplate_returns_default_templates() + { + var contextTemplate = Templates.GetDefaultTemplate(Templates.ContextTemplate); + Assert.False(string.IsNullOrWhiteSpace(contextTemplate)); + + var entityTemplate = Templates.GetDefaultTemplate(Templates.EntityTemplate); + Assert.False(string.IsNullOrWhiteSpace(entityTemplate)); + + var mappingTemplate = Templates.GetDefaultTemplate(Templates.MappingTemplate); + Assert.False(string.IsNullOrWhiteSpace(mappingTemplate)); + } + } +} diff --git a/test/PowerTools.Test/packages.config b/test/PowerTools.Test/packages.config new file mode 100644 index 00000000..28c4688a --- /dev/null +++ b/test/PowerTools.Test/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file