Skip to content

Commit

Permalink
Extracted to a separate project; use sqlite to stub out entityframewo…
Browse files Browse the repository at this point in the history
…rkcore for testing.
  • Loading branch information
NicholasMTElliott committed Aug 14, 2017
1 parent 4c41e68 commit 25ad720
Show file tree
Hide file tree
Showing 37 changed files with 3,071 additions and 0 deletions.
63 changes: 63 additions & 0 deletions .gitattributes
@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary

###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
134 changes: 134 additions & 0 deletions dotnet/PopcornCore/Popcorn+EntityFrameworkCore.cs
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using System.Reflection;

namespace Skyward.Popcorn.Core
{
public static class EntityFrameworkCore
{
private const string DbCountKey = "DbCount";
private const string DbKey = "Db";


/// <summary>
/// Helper function creating a mapping from a source type to a destination type. Will attempt to auto-load navigation properties as needed.
/// </summary>
/// <param name="optionsBuilder"></param>
/// <param name="defaultIncludes"></param>
/// <param name="self">todo: describe self parameter on MapEntityFramework</param>
/// <typeparam name="TSourceType"></typeparam>
/// <typeparam name="TDestType"></typeparam>
/// <typeparam name="TContext"></typeparam>
/// <returns></returns>
public static PopcornConfiguration MapEntityFramework<TSourceType, TDestType, TContext>(
this PopcornConfiguration self,
DbContextOptionsBuilder<TContext> optionsBuilder,
string defaultIncludes = null)
where TContext : DbContext
where TSourceType : class
where TDestType : class
{
return self.Map<TSourceType, TDestType>(defaultIncludes, (definition) =>
{
definition
// Before dealing with this object, create a context to use
.BeforeExpansion((destinationObject, sourceObject, context) =>
{
// Do some reference counting here
if (!context.ContainsKey(DbKey))
{
DbContext db = ConstructDbContextWithOptions(optionsBuilder);
try
{
db.Attach(sourceObject);
context[DbKey] = db;
context[DbCountKey] = 1;
}
catch { }
}
else
{
context[DbCountKey] = (int)context[DbCountKey] + 1;
}
})
// Afterwards clean up our resources
.AfterExpansion((destinationObject, sourceObject, context) =>
{
if (context.ContainsKey(DbKey))
{
// If the reference count goes to 0, destroy the context
var decrementedReferenceCount = (int)context[DbCountKey] - 1;
if (decrementedReferenceCount == 0)
{
(context[DbKey] as IDisposable).Dispose();
context.Remove(DbKey);
context.Remove(DbCountKey);
}
else
{
context[DbCountKey] = decrementedReferenceCount;
}
}
});
// Now, find all navigation properties on this type and configure each one to load from the database if
// actually requested for expansion
using (DbContext db = ConstructDbContextWithOptions(optionsBuilder))
{
foreach (var prop in typeof(TSourceType).GetNavigationProperties(db))
{
definition.PrepareProperty(prop.Name, (destinationObject, destinationProperty, sourceObject, context) =>
{
if (context.ContainsKey(DbKey))
{
var expandDb = context[DbKey] as TContext;
expandDb.Attach(sourceObject as TSourceType);
expandDb.Entry(sourceObject as TSourceType).Collection(prop.Name).Load();
}
});
}
}
});
}

/// <summary>
/// A helper method to build a DbContext given an options builder.
/// </summary>
/// <typeparam name="TContext"></typeparam>
/// <param name="optionsBuilder"></param>
/// <returns></returns>
private static DbContext ConstructDbContextWithOptions<TContext>(DbContextOptionsBuilder<TContext> optionsBuilder) where TContext : DbContext
{
var constructor = typeof(TContext).GetConstructor(new Type[] { typeof(DbContextOptions<TContext>) });
var db = (DbContext)constructor.Invoke(new[] { optionsBuilder.Options });
return db;
}


/// <summary>
/// Method that uses a DbContext to get a list of 'Navigation' properties -- that is, properties that represent other entities
/// rather than strictly data on THIS entity.
/// </summary>
/// <param name="entityType"></param>
/// <param name="context"></param>
/// <returns></returns>
public static List<PropertyInfo> GetNavigationProperties(this Type entityType, DbContext context)
{
var properties = new List<PropertyInfo>();
//Get the System.Data.Entity.Core.Metadata.Edm.EntityType
//associated with the entity.
var entityElementType = context.Model.FindEntityType(entityType);

//Iterate each
//System.Data.Entity.Core.Metadata.Edm.NavigationProperty
//in EntityType.NavigationProperties, get the actual property
//using the entityType name, and add it to the return set.
foreach (var navigationProperty in entityElementType.GetNavigations())
{
properties.Add(entityType.GetProperty(navigationProperty.Name));
}
return properties;
}
}
}
18 changes: 18 additions & 0 deletions dotnet/PopcornCore/PopcornCore.csproj
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" />
<PackageReference Include="Serilog" Version="2.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PopcornStandard\PopcornStandard.csproj" />
</ItemGroup>

</Project>
129 changes: 129 additions & 0 deletions dotnet/PopcornCore/PopcornJsonFormatter.cs
@@ -0,0 +1,129 @@
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
using System.Collections.Generic;
using System.Linq;

namespace Skyward.Popcorn.Core
{
/// <summary>
/// This formatter provides an interception point on data that is returned by an API method. It is the point of conversion into the serialized format.
/// This allows us to affect and control what actually ends up being serialized and returned as a Response.
///
/// It relies on an existing JsonFormatter which we will pass through to.
/// </summary>
internal class PopcornJsonFormatter : TextOutputFormatter
{
TextOutputFormatter _innerFormatter;
Expander _expander;
Dictionary<string, object> _context;
Func<object, object, object> _inspector;

/// <summary>
/// The only constructor
/// </summary>
/// <param name="innerFormatter">the formatter to replace</param>
/// <param name="expander">The constructed and configured expander</param>
/// <param name="expandContext">Any context to be passed in</param>
/// <param name="inspector">Any inspector to wrap around the results.</param>
public PopcornJsonFormatter(TextOutputFormatter innerFormatter, Expander expander, Dictionary<string, object> expandContext = null, Func<object, object, object> inspector = null) :
base()
{
_innerFormatter = innerFormatter;
_expander = expander;
_context = expandContext;
_inspector = inspector;

// Duplicate the underlying supported types and encodings
foreach (var mediaType in innerFormatter.SupportedMediaTypes)
SupportedMediaTypes.Add(mediaType);

foreach (var encoding in innerFormatter.SupportedEncodings)
SupportedEncodings.Add(encoding);
}

/// <summary>
/// Pass through the query to our inner formatter
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
return _innerFormatter.CanWriteResult(context);
}

/// <summary>
/// Handle receiving an object and writing a response
/// </summary>
/// <param name="context"></param>
/// <param name="selectedEncoding"></param>
/// <returns></returns>
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
// See if our expander will replace this object
var replacementContext = context;
if (_expander.WillExpand(context.Object))
{
// see if we can find some include statements
string includes = "[]";
if (context.HttpContext.Request.Query.ContainsKey("include"))
{
includes = context.HttpContext.Request.Query["include"];
}
else if (context.HttpContext.Request.Headers?.ContainsKey("API-INCLUDE") ?? false)
{
includes = context.HttpContext.Request.Headers["API-INCLUDE"];
}

// Use our expander and expand the object
var expanded = _expander.Expand(context.Object, _context, PropertyReference.Parse(includes));

// Apply our inspector to the expanded content
if (_inspector != null)
expanded = _inspector(expanded, _context);

// And create a new context that we'll pass into our inner formatter
replacementContext = new OutputFormatterWriteContext(
context.HttpContext,
context.WriterFactory,
context.ObjectType,
expanded
);
}

return _innerFormatter.WriteResponseBodyAsync(replacementContext, selectedEncoding);
}
}

/// <summary>
/// Some useful extensions for Web App style configuration
/// </summary>
public static class ApiExpanderJsonFormatterExtensions
{
/// <summary>
/// Configure the AspNet Core MVC options to include an Api Expander. Allow the caller to configure it with an action.
/// </summary>
/// <param name="options"></param>
/// <param name="configure"></param>
public static void ConfigureApiExpansion(this Microsoft.AspNetCore.Mvc.MvcOptions options, Action<PopcornConfiguration> configure = null)
{
// Inject our Api Expander
// First we remove the existing one
var existingJsonFormatter = options.OutputFormatters.First(of => of is JsonOutputFormatter) as JsonOutputFormatter;
options.OutputFormatters.RemoveType<JsonOutputFormatter>();

// Create an expander object
var expander = new Expander();
var configuration = new PopcornConfiguration(expander);
// optionally configure this expander
if (configure != null)
{
configure(configuration);
}

// And add a Json Formatter that will utilize that expander when appropriate
options.OutputFormatters.Add(new PopcornJsonFormatter(existingJsonFormatter, expander, configuration.Context, configuration.Inspector));
}
}
}

0 comments on commit 25ad720

Please sign in to comment.