Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[ODS-4799] Introduce resource POST/Retry order in dependency endpoint #1149

Merged
merged 5 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions Application/EdFi.Ods.Api/Models/GraphML/GraphMLEdge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,71 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using System;
using EdFi.Ods.Common.Models.Domain;
using QuickGraph;

namespace EdFi.Ods.Api.Models.GraphML
{
// ReSharper disable once InconsistentNaming
public class GraphMLEdge
public class GraphMLEdge : IEquatable<GraphMLEdge>, IEdge<GraphMLNode>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The GraphML model used for serialization needed to be processed as a bidirectional graph with the BreakCycles logic (an extension method written for bidirectional graphs) after introducing the /ed-fi/staffs#Retry node into the graph.

In moving the dependencies of the staff employment/assignment associations to be dependents instead of the new "Retry' node, a "soft" (optional) circular dependency on between the Staff "Retry" node and the StaffEdOrgAssignmentAssociation resulted because of the StaffEdOrgAssignmentAssociation's optional dependency on the StaffEdOrgEmploymentAssociation. This circular dependency needed to be removed reusing the BreakCycles logic mentioned above, and requiring GraphML models to support the behavior required by the graph library (IEquatable<T> and for the node, IComparable<T>).

image

{
public GraphMLEdge(string source, string target)
public GraphMLEdge(GraphMLNode source, GraphMLNode target)
{
Source = source;
Target = target;
}

public string Source { get; }
public GraphMLEdge(GraphMLNode source, GraphMLNode target, AssociationView edgeAssociationView)
: this(source, target)
{
AssociationView = edgeAssociationView;
}

public AssociationView AssociationView { get; }

public GraphMLNode Source { get; }

public GraphMLNode Target { get; }

#region Equality members
public bool Equals(GraphMLEdge other)
{
if (other is null)
{
return false;
}

if (ReferenceEquals(this, other))
{
return true;
}

return Source.Id == other.Source.Id && Target.Id == other.Target.Id;
}

public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}

if (ReferenceEquals(this, obj))
{
return true;
}

if (obj.GetType() != GetType())
{
return false;
}

return Equals((GraphMLEdge)obj);
}

public string Target { get; }
public override int GetHashCode() => HashCode.Combine(Source, Target);

#endregion
}
}
90 changes: 89 additions & 1 deletion Application/EdFi.Ods.Api/Models/GraphML/GraphMLNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,99 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using System;

namespace EdFi.Ods.Api.Models.GraphML
{
// ReSharper disable once InconsistentNaming
public class GraphMLNode
public class GraphMLNode : IEquatable<GraphMLNode>, IComparable<GraphMLNode>, IComparable
{
public GraphMLNode() { }

public GraphMLNode(string id)
{
Id = id;
}

public string Id { get; set; }

public override string ToString() => Id;

#region Equality members

public bool Equals(GraphMLNode other)
{
if (other is null)
{
return false;
}

if (ReferenceEquals(this, other))
{
return true;
}

return Id == other.Id;
}

public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}

if (ReferenceEquals(this, obj))
{
return true;
}

if (obj.GetType() != GetType())
{
return false;
}

return Equals((GraphMLNode)obj);
}

public override int GetHashCode() => (Id != null
? Id.GetHashCode()
: 0);

#endregion

#region Comparable members
public int CompareTo(GraphMLNode other)
{
if (ReferenceEquals(this, other))
{
return 0;
}

if (other is null)
{
return 1;
}

return string.Compare(Id, other.Id, StringComparison.OrdinalIgnoreCase);
}

public int CompareTo(object obj)
{
if (obj is null)
{
return 1;
}

if (ReferenceEquals(this, obj))
{
return 0;
}

return obj is GraphMLNode other
? CompareTo(other)
: throw new ArgumentException($"Object must be of type {nameof(GraphMLNode)}");
}
#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Linq;
using EdFi.Ods.Common.Conventions;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Extensions;
using EdFi.Ods.Common.Models.Domain;
using EdFi.Ods.Common.Models.Graphs;
using EdFi.Ods.Common.Models.Resource;
Expand Down Expand Up @@ -36,15 +35,13 @@ private void ApplyStaffTransformation(BidirectionalGraph<Resource, AssociationVi
var staffResource =
resources.FirstOrDefault(x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Staff"));

var staffEdOrgEmployAssoc =
resources.FirstOrDefault(
x => x.FullName == new FullName(
EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationEmploymentAssociation"));
var staffEdOrgEmployAssoc = resources.FirstOrDefault(
x => x.FullName
== new FullName(EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationEmploymentAssociation"));

var staffEdOrgAssignAssoc =
resources.FirstOrDefault(
x => x.FullName == new FullName(
EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationAssignmentAssociation"));
var staffEdOrgAssignAssoc = resources.FirstOrDefault(
x => x.FullName
== new FullName(EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationAssignmentAssociation"));

// No staff entity in the graph, nothing to do.
if (staffResource == null)
Expand Down Expand Up @@ -78,18 +75,16 @@ private void ApplyStaffTransformation(BidirectionalGraph<Resource, AssociationVi

resourceGraph.AddEdge(
new AssociationViewEdge(
staffEdOrgAssignAssoc, directStaffDependency.Target, directStaffDependency.AssociationView));
staffEdOrgAssignAssoc,
directStaffDependency.Target,
directStaffDependency.AssociationView));

resourceGraph.AddEdge(
new AssociationViewEdge(
staffEdOrgEmployAssoc, directStaffDependency.Target, directStaffDependency.AssociationView));
staffEdOrgEmployAssoc,
directStaffDependency.Target,
directStaffDependency.AssociationView));
}

// Add StaffEducationOrganizationAssignmentAssociations/#POSTRetry node for Staff resource
AddPostRetryVertexForResource(resourceGraph, staffEdOrgAssignAssoc, staffResource);

// Add StaffEducationOrganizationEmploymentAssociations/#POSTRetry node for Staff resource
AddPostRetryVertexForResource(resourceGraph, staffEdOrgEmployAssoc, staffResource);
}

private static void ApplyStudentTransformation(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph)
Expand Down Expand Up @@ -131,21 +126,18 @@ private static void ApplyStudentTransformation(BidirectionalGraph<Resource, Asso

resourceGraph.AddEdge(
new AssociationViewEdge(
studentSchoolAssociationResource, directStudentDependency.Target,
studentSchoolAssociationResource,
directStudentDependency.Target,
directStudentDependency.AssociationView));
}

// Add a StudentSchoolAssociation/#POSTRetry node for Student resource
AddPostRetryVertexForResource(resourceGraph, studentSchoolAssociationResource, studentResource);
}

private void ApplyContactTransformation(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph)
{
var resources = resourceGraph.Vertices.ToList();

var contactResource = resources.FirstOrDefault(
x =>
x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Contact")
x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Contact")
|| x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Parent"));

// No entity named Parent or Contact in the graph, nothing to do.
Expand Down Expand Up @@ -190,13 +182,11 @@ private void ApplyContactTransformation(BidirectionalGraph<Resource, Association

resourceGraph.AddEdge(
new AssociationViewEdge(
studentContactAssociationResource, directContactDependency.Target,
studentContactAssociationResource,
directContactDependency.Target,
directContactDependency.AssociationView));
}

// Add a StudentContactAssociationResource/#POSTRetry node for Contact resource
AddPostRetryVertexForResource(resourceGraph, studentContactAssociationResource, contactResource);


string LogAndThrowException()
{
string message =
Expand All @@ -210,18 +200,5 @@ string LogAndThrowException()
message: message);
}
}

private static void AddPostRetryVertexForResource(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph, Resource postRetrySource,
Resource postRetryTarget)
{
var postRetryVertex = new Resource(postRetrySource.Name);
postRetryVertex.IsPostRetryResource = true;

postRetryVertex.PostRetryOriginalSchemaUriSegment =
postRetrySource.SchemaUriSegment();

resourceGraph.AddVertex(postRetryVertex);
resourceGraph.AddEdge(new AssociationViewEdge(postRetryVertex, postRetryTarget, null));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public static bool IsDerivedFrom(
public static string SchemaUriSegment(this Resource resource) => resource
.ResourceModel?.SchemaNameMapProvider
?.GetSchemaMapByPhysicalName(resource.FullName.Schema)
.UriSegment ?? resource.PostRetryOriginalSchemaUriSegment;
.UriSegment;

/// <summary>
/// Check if resource is abstract.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ public interface IResourceLoadGraphFactory
/// Creates a new graph containing the Ed-Fi model's resources, performing any defined graph transformations.
/// </summary>
/// <returns>A new graph instance.</returns>
BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph(bool includePostRetryNodes = true);
BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ public ResourceLoadGraphFactory(IResourceModelProvider resourceModelProvider,
_resourceModelProvider = resourceModelProvider;
_graphTransformers = graphTransformers;
}
public BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph(bool includePostRetryNodes)

public BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph()
{
var resourceModel = _resourceModelProvider.GetResourceModel();

var resourceGraph = new BidirectionalGraph<Resource.Resource, AssociationViewEdge>();

var resources = resourceModel.GetAllResources()
.Where(r => !r.IsAbstract() && !r.FullName.IsEdFiSchoolYearType())
.ToArray();

resourceGraph.AddVertexRange(resources);

var edges = resources
Expand All @@ -59,7 +59,7 @@ public ResourceLoadGraphFactory(IResourceModelProvider resourceModelProvider,

// Eliminate redundant edges
.Distinct(AssociationViewEdge.Comparer);

resourceGraph.AddEdgeRange(edges.Where(e => !e.Source.FullName.IsEdFiSchoolYearType()));

// Apply predefined graph transformations
Expand All @@ -70,30 +70,10 @@ public ResourceLoadGraphFactory(IResourceModelProvider resourceModelProvider,
graphTransformer.Transform(resourceGraph);
}
}

resourceGraph.BreakCycles(edge => edge.AssociationView?.IsSoftDependency ?? false);

if (!includePostRetryNodes)
{
RemovePostRetryNodes();
}

return resourceGraph;

void RemovePostRetryNodes()
{
var postRetryVertices = resourceGraph.Vertices.Where(v => v.IsPostRetryResource).ToList();
resourceGraph.BreakCycles(edge => edge.AssociationView.IsSoftDependency);

foreach(var postRetryVertex in postRetryVertices)
{
var outEdges = resourceGraph.OutEdges(postRetryVertex).ToList();
foreach (var edge in outEdges)
{
resourceGraph.RemoveEdge(edge);
}
resourceGraph.RemoveVertex(postRetryVertex);
}
}
return resourceGraph;
}
}

Expand Down Expand Up @@ -140,7 +120,7 @@ public static IEnumerable<AssociationViewEdge> CreateEdges(Reference reference)
}
}

public override string ToString() => AssociationView.Association.ToString();
public override string ToString() => $"({Source.FullName}) --> ({Target.FullName})";

private sealed class AssociationViewEdgeEqualityComparer : IEqualityComparer<AssociationViewEdge>
{
Expand Down
4 changes: 0 additions & 4 deletions Application/EdFi.Ods.Common/Models/Resource/Resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ public Resource(string name)
}

public bool IsEdFiCore { get; set; }

public bool IsPostRetryResource { get; set; }

public string PostRetryOriginalSchemaUriSegment { get; set; }

/// <summary>
/// Gets the root <see cref="Resource" /> class for the current resource.
Expand Down
Loading
Loading