Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

support entity type filtration for the entities not included into sets #28

Merged
merged 3 commits into from
Nov 26, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 116 additions & 72 deletions EDMXTrimmer/EDMXTrimmer/EdmxTrimmer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class EdmxTrimmer
private XmlDocument _xmlDocument;
private XmlNode _firstSchemaNode;
private string ENTITYNAMESPACE;
private string ENTITYNAMESPACE_ALIAS;
private const string TAG_SCHEMA = "Schema";
private const string TAG_ENTITY_TYPE = "EntityType";
private const string TAG_ENTITY_SET = "EntitySet";
Expand All @@ -31,12 +32,15 @@ class EdmxTrimmer
private const string TAG_PARAMETER = "Parameter";
private const string TAG_ENUM_TYPE = "EnumType";
private const string TAG_ACTION_IMPORT = "ActionImport";
private const string ATTRIBUTE_ALIAS = "Alias";
private const string ATTRIBUTE_NAMESPACE = "Namespace";
private const string ATTRIBUTE_NAME = "Name";
private const string ATTRIBUTE_TYPE = "Type";
private const string ATTRIBUTE_TARGET = "Target";
private const string ATTRIBUTE_AXType = "AXType";

private readonly IDictionary<string, Regex> entityTypeRegexps = new Dictionary<string, Regex>();

public EdmxTrimmer(
string edmxFile,
string outputFileName,
Expand Down Expand Up @@ -81,6 +85,11 @@ public void AnalyzeFile()
this._firstSchemaNode = this._xmlDocument.GetElementsByTagName(TAG_SCHEMA)[0];
this.ENTITYNAMESPACE = this._firstSchemaNode.Attributes[ATTRIBUTE_NAMESPACE].Value + ".";

var aliasAttrValue = this._firstSchemaNode.Attributes[ATTRIBUTE_ALIAS]?.Value.Trim();
if(!string.IsNullOrEmpty(aliasAttrValue)) {
this.ENTITYNAMESPACE_ALIAS = aliasAttrValue + ".";
}

var entitySets = this._xmlDocument.GetElementsByTagName(TAG_ENTITY_SET).Cast<XmlNode>().ToList();
var entityTypes = this._xmlDocument.GetElementsByTagName(TAG_ENTITY_TYPE).Cast<XmlNode>().ToList();
var originalEntityCount = entitySets.Count;
Expand Down Expand Up @@ -125,31 +134,48 @@ private void RemoveAllEntitiesExcept(
List<XmlNode> entitySets,
List<XmlNode> entityTypes)
{
string regex = EntitySearchTermsToRegularExpression(entitiesToKeep);
var entitiesKeep = entitySets.Where(n => Regex.IsMatch(n.Attributes[ATTRIBUTE_NAME].Value, regex)).ToList();
var (entitySetsToKeep, entityTypeNamesToKeep) = FilterByEntity(entitiesToKeep, entitySets, entityTypes, true);

RemoveEntitySets(entitySets, entitiesKeep);
RemoveEntityTypes(entityTypes, entitiesKeep);
RemoveEntitySets(entitySets, entitySetsToKeep);
RemoveEntityTypes(entityTypes, entityTypeNamesToKeep);
}

private void RemoveExcludedEntities(
List<string> entitiesToExclude,
List<XmlNode> entitySets,
List<XmlNode> entityTypes)
{
string regex = EntitySearchTermsToRegularExpression(entitiesToExclude);
var entitiesKeep = entitySets.Where(n => !Regex.IsMatch(n.Attributes[ATTRIBUTE_NAME].Value, regex)).ToList();
var (entitySetsToKeep, entityTypeNamesToKeep) = FilterByEntity(entitiesToExclude, entitySets, entityTypes, false);

RemoveEntitySets(entitySets, entitiesKeep);
RemoveEntityTypes(entityTypes, entitiesKeep);
RemoveEntitySets(entitySets, entitySetsToKeep);
RemoveEntityTypes(entityTypes, entityTypeNamesToKeep);
}

private string EntitySearchTermsToRegularExpression(List<string> entitiesToKeep)
private (List<XmlNode> EntitySetsToKeep, IReadOnlyCollection<string> EntityTypeNamesToKeep) FilterByEntity(IEnumerable<string> filteringEntities, IEnumerable<XmlNode> entitySets, IEnumerable<XmlNode> entityTypes, bool includeFiltered)
{
List<string> listRegularExpression = entitiesToKeep.Select(s => EntitySearchTermToRegularExpression(s)).ToList();
string regex = String.Join("|", listRegularExpression.ToArray());
var nameRegex = EntitySearchTermsToRegularExpression(filteringEntities);

return regex;
var entitySetsNodes = entitySets
.Where(n => Regex.IsMatch(n.Attributes[ATTRIBUTE_NAME].Value, nameRegex) ? includeFiltered : !includeFiltered)
.ToList();

var entityTypeNames = entitySetsNodes
.Select(n => GetEntityTypeWithoutNamespace(n, TAG_ENTITY_TYPE))
.Concat(entityTypes
.Where(node => Regex.IsMatch(node.Attributes[ATTRIBUTE_NAME].Value, nameRegex) ? includeFiltered : !includeFiltered)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧

Thanks again for the contributions. I had a chance to look at the commits of the smaller pr in detail. Looks pretty good so far.

This part needs some work, I think. I did some regression testing with D365FO edmx files and noticed that with the --entitiestoexclude option, the entities remained in the resulting file.

This seems to be caused by this change in how the entityTypeNames are determined. Previously, this was based only on the filtered entity sets (see below for the original logic). Now, it also adds the names from the directly filtered entity types. However, the entity type names are in singular, while the entity set name is in plural. The entity names for the include/exclude parameters so far have been given in plural. For the new version to work, they probably would need to be given both in plural and in singular. However, I'm not sure if that would work as filter for both entity sets and types.

Since the intent of this change is to enable filtering of entities that are not part of sets, one option I can think of is to provide additional parameters where the singular entity names are specified. Those would then only be applied to entitiy types that are not part of an entity set. Another simpler one might be to provide a --excludeentitiesnotinsets parameter to remove entities that are not part of sets alltogether.

List<String> entityTypesFound = new List<string>();
entitiesKeep.ForEach(n =>
{
string entityType = n.Attributes[TAG_ENTITY_TYPE].Value;
entityType = entityType.Replace(ENTITYNAMESPACE, "");
entityTypesFound.Add(entityType);
});

.Select(node => GetEntityTypeWithoutNamespace(node, ATTRIBUTE_NAME))
)
.Distinct()
.ToList();

return (entitySetsNodes, entityTypeNames);
}

private string EntitySearchTermsToRegularExpression(IEnumerable<string> entitiesToKeep)
{
var parts = entitiesToKeep.Select(EntitySearchTermToRegularExpression);

return String.Join("|", parts);
}

private string EntitySearchTermToRegularExpression(string searchTerm)
Expand All @@ -164,7 +190,7 @@ private string EntitySearchTermToRegularExpression(string searchTerm)
return regex;
}

private void RemoveEntitySets(List<XmlNode> entitySets, List<XmlNode> entitiesKeep)
private void RemoveEntitySets(IEnumerable<XmlNode> entitySets, List<XmlNode> entitiesKeep)
{
// Remove entities not required (EntitySet)
entitySets.Except(entitiesKeep).ToList().ForEach(n => n.ParentNode.RemoveChild(n));
Expand All @@ -179,81 +205,42 @@ private void RemoveEntitySets(List<XmlNode> entitySets, List<XmlNode> entitiesKe
});
}

private void RemoveEntityTypes(List<XmlNode> entityTypes, List<XmlNode> entitiesKeep)
private void RemoveEntityTypes(IReadOnlyCollection<XmlNode> entityTypes, IReadOnlyCollection<string> entitiesNamesToKeep)
{
List<String> entityTypesFound = new List<string>();
entitiesKeep.ForEach(n =>
{
string entityType = n.Attributes[TAG_ENTITY_TYPE].Value;
entityType = entityType.Replace(ENTITYNAMESPACE, "");
entityTypesFound.Add(entityType);
});

// Remove all navigation properties
this._xmlDocument.GetElementsByTagName(TAG_NAVIGATION_PROPERTY).Cast<XmlNode>()
.Where(navProp => !entityTypesFound.Any(entityType => EntityExists(navProp, entityType))).ToList()
.Where(navProp => !entitiesNamesToKeep.Any(entityType => EntityExists(navProp, entityType))).ToList()
.ForEach(n => n.ParentNode.RemoveChild(n));

// Remove entity not required (EntityType)
var entityTypesKeep = entityTypes.Where(n => entityTypesFound.Contains(n.Attributes[ATTRIBUTE_NAME].Value)).ToList();
entityTypes.Except(entityTypesKeep).ToList().ForEach(n => n.ParentNode.RemoveChild(n));

var entityTypesToKeep = entityTypes.Where(n => entitiesNamesToKeep.Contains(n.Attributes[ATTRIBUTE_NAME].Value)).ToList();
entityTypes.Except(entityTypesToKeep).ToList().ForEach(n => n.ParentNode.RemoveChild(n));
// Remove all Actions
this._xmlDocument.GetElementsByTagName(TAG_ACTION).Cast<XmlNode>()
.Where(action => !entityTypesFound.Any(entityType => action.ChildNodes.Cast<XmlNode>().
.Where(action => !entitiesNamesToKeep.Any(entityType => action.ChildNodes.Cast<XmlNode>().
Any(childNode => EntityExists(childNode, entityType)))).ToList()
.ForEach(n => n.ParentNode.RemoveChild(n));

// Determine enums to keep
List<String> enumTypesFound = new List<string>();
// Enums from entity type properties
entityTypesKeep.ForEach(n =>
{
var properties = n.ChildNodes.Cast<XmlNode>().Where(prop => prop.Name.Equals(TAG_PROPERTY)).ToList();
properties.ForEach(prop =>
{
if (prop.Attributes[ATTRIBUTE_TYPE] != null)
{
var enumType = prop.Attributes[ATTRIBUTE_TYPE].Value;
if (enumType.StartsWith(ENTITYNAMESPACE))
{
enumType = enumType.Replace(ENTITYNAMESPACE, "");
enumTypesFound.Add(enumType);
}
}
});
});

var propertiesTypes = entityTypesToKeep.SelectMany(typeNode => GetEntityTypesFromNodeChildren(typeNode, TAG_PROPERTY));
enumTypesFound.AddRange(propertiesTypes);

// Enums from actions
var entityActions = this._xmlDocument.GetElementsByTagName(TAG_ACTION).Cast<XmlNode>().ToList();
entityActions.ForEach(action =>
entityActions.ForEach(actionNode =>
{
// Enums from parameters
var parameters = action.ChildNodes.Cast<XmlNode>().Where(param => param.Name.Equals(TAG_PARAMETER)).ToList();
parameters.ForEach(param =>
{
if (param.Attributes[ATTRIBUTE_TYPE] != null)
{
var enumType = param.Attributes[ATTRIBUTE_TYPE].Value;
if (enumType.StartsWith(ENTITYNAMESPACE))
{
enumType = enumType.Replace(ENTITYNAMESPACE, "");
enumTypesFound.Add(enumType);
}
}
});
var parametersTypes = GetEntityTypesFromNodeChildren(actionNode, TAG_PARAMETER)!;
enumTypesFound.AddRange(parametersTypes);

// Enum from return type
// get the first child node with name "ReturnType" if it exists
var returnType = action.ChildNodes.Cast<XmlNode>().FirstOrDefault(node => node.Name.Equals(TAG_RETURN_TYPE));
if (returnType != null && returnType.Attributes[ATTRIBUTE_TYPE] != null)
{
var enumType = returnType.Attributes[ATTRIBUTE_TYPE].Value;
if (enumType.StartsWith(ENTITYNAMESPACE))
{
enumType = enumType.Replace(ENTITYNAMESPACE, "");
enumTypesFound.Add(enumType);
}
}

var returnType = GetEntityTypesFromNodeChildren(actionNode, TAG_RETURN_TYPE)!;
enumTypesFound.Add(returnType.FirstOrDefault());
});
// Remove unused Enums except AXType
this._xmlDocument.GetElementsByTagName(TAG_ENUM_TYPE).Cast<XmlNode>()
Expand All @@ -263,7 +250,45 @@ private void RemoveEntityTypes(List<XmlNode> entityTypes, List<XmlNode> entities
.ForEach(n => n.ParentNode.RemoveChild(n));

this._xmlDocument.Save(OutputFileName);

return;

IReadOnlyCollection<string> GetEntityTypesFromNodeChildren(XmlNode typeNode, string nodeName) =>
typeNode
.ChildNodes
.Cast<XmlNode>()
.Where(prop => prop.Name.Equals(nodeName))
.Select(RemoveNamespace)
.Where(name => name != null)
.ToList();

string RemoveNamespace(XmlNode xmlNode) {
var enumType = xmlNode.Attributes[ATTRIBUTE_TYPE]?.Value;

if(enumType == null) {
return null;
}
if(ENTITYNAMESPACE_ALIAS != null && enumType.StartsWith(ENTITYNAMESPACE_ALIAS)) {
return enumType.Replace(ENTITYNAMESPACE_ALIAS, "");
}
if(enumType.StartsWith(ENTITYNAMESPACE)) {
return enumType.Replace(ENTITYNAMESPACE, "");
}

return null;
}
}

private string GetEntityTypeWithoutNamespace(XmlNode n, string attributeName) {
var entityType = n.Attributes[attributeName]?.Value;

if(ENTITYNAMESPACE_ALIAS != null) {
var replaced = entityType.Replace(ENTITYNAMESPACE_ALIAS, "");
if(replaced != entityType) {
return replaced;
}
}
return entityType.Replace(ENTITYNAMESPACE, "");
}

private void RemovePrimaryAnnotations()
Expand All @@ -282,9 +307,28 @@ private void RemoveActionImports()
.ForEach(n => n.ParentNode.RemoveChild(n));
}

private bool EntityExists(XmlNode xmlNode, string entityType)
{
return xmlNode.Attributes[ATTRIBUTE_TYPE] == null ? false : Regex.IsMatch(xmlNode.Attributes[ATTRIBUTE_TYPE].Value, ENTITYNAMESPACE + entityType + "\\)?$");
private bool EntityExists(XmlNode xmlNode, string entityType) {
var typeValue = xmlNode.Attributes[ATTRIBUTE_TYPE]?.Value;

if(null == typeValue) {
return false;
}
if(ENTITYNAMESPACE_ALIAS != null && IsEntityTypeMatches(entityType, ENTITYNAMESPACE_ALIAS, typeValue)) {
return true;
}
return IsEntityTypeMatches(entityType, ENTITYNAMESPACE, typeValue);
}

private bool IsEntityTypeMatches(string entityType, string @namespace, string source) {
var key = @namespace + entityType;

if(!entityTypeRegexps.TryGetValue(key, out var regex)) {
var pattern = Regex.Escape(@namespace + entityType) + "\\)?$";
regex = new Regex(pattern);
entityTypeRegexps.Add(key, regex);
}

return regex.IsMatch(source!);
}
}
}
}