Skip to content

Commit 067dcad

Browse files
authored
Merge pull request #10 from Research-Institute/feature/controller-override
v0.1.0
2 parents 4cc00b0 + a464431 commit 067dcad

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1113
-204
lines changed

.travis.yml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
language: csharp
2+
sudo: required
3+
dist: trusty
4+
env:
5+
- CLI_VERSION=latest
6+
addons:
7+
apt:
8+
packages:
9+
- gettext
10+
- libcurl4-openssl-dev
11+
- libicu-dev
12+
- libssl-dev
13+
- libunwind8
14+
- zlib1g
15+
mono:
16+
- 4.2.3
17+
os:
18+
- linux
19+
- osx
20+
osx_image: xcode7.1
21+
branches:
22+
only:
23+
- master
24+
before_install:
25+
- if test "$TRAVIS_OS_NAME" = "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi
26+
install:
27+
- export DOTNET_INSTALL_DIR="$PWD/.dotnetcli"
28+
- curl -sSL https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0/scripts/obtain/dotnet-install.sh | bash /dev/stdin --version "$CLI_VERSION" --install-dir "$DOTNET_INSTALL_DIR"
29+
- export PATH="$DOTNET_INSTALL_DIR:$PATH"
30+
script:
31+
- ./build.sh

Build.ps1

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<#
2+
.SYNOPSIS
3+
You can add this to you build script to ensure that psbuild is available before calling
4+
Invoke-MSBuild. If psbuild is not available locally it will be downloaded automatically.
5+
#>
6+
function EnsurePsbuildInstalled{
7+
[cmdletbinding()]
8+
param(
9+
[string]$psbuildInstallUri = 'https://raw.githubusercontent.com/ligershark/psbuild/master/src/GetPSBuild.ps1'
10+
)
11+
process{
12+
if(-not (Get-Command "Invoke-MsBuild" -errorAction SilentlyContinue)){
13+
'Installing psbuild from [{0}]' -f $psbuildInstallUri | Write-Verbose
14+
(new-object Net.WebClient).DownloadString($psbuildInstallUri) | iex
15+
}
16+
else{
17+
'psbuild already loaded, skipping download' | Write-Verbose
18+
}
19+
20+
# make sure it's loaded and throw if not
21+
if(-not (Get-Command "Invoke-MsBuild" -errorAction SilentlyContinue)){
22+
throw ('Unable to install/load psbuild from [{0}]' -f $psbuildInstallUri)
23+
}
24+
}
25+
}
26+
27+
# Taken from psake https://github.com/psake/psake
28+
29+
<#
30+
.SYNOPSIS
31+
This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode
32+
to see if an error occcured. If an error is detected then an exception is thrown.
33+
This function allows you to run command-line programs without having to
34+
explicitly check the $lastexitcode variable.
35+
.EXAMPLE
36+
exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed"
37+
#>
38+
function Exec
39+
{
40+
[CmdletBinding()]
41+
param(
42+
[Parameter(Position=0,Mandatory=1)][scriptblock]$cmd,
43+
[Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd)
44+
)
45+
& $cmd
46+
if ($lastexitcode -ne 0) {
47+
throw ("Exec: " + $errorMessage)
48+
}
49+
}
50+
51+
if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse }
52+
53+
EnsurePsbuildInstalled
54+
55+
exec { & dotnet restore }
56+
57+
Invoke-MSBuild
58+
59+
$revision = @{ $true = $env:APPVEYOR_BUILD_NUMBER; $false = 1 }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL];
60+
$revision = "{0:D4}" -f [convert]::ToInt32($revision, 10)
61+
62+
exec { & dotnet test .\JsonApiDotNetCoreTests -c Release }
63+
64+
exec { & dotnet pack .\JsonApiDotNetCore -c Release -o .\artifacts --version-suffix=$revision }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Routing;
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace JsonApiDotNetCore.Abstractions
7+
{
8+
public interface IJsonApiContext
9+
{
10+
JsonApiModelConfiguration Configuration { get; }
11+
object DbContext { get; }
12+
HttpContext HttpContext { get; }
13+
Route Route { get; }
14+
string GetEntityName();
15+
Type GetEntityType();
16+
Type GetJsonApiResourceType();
17+
}
18+
}

JsonApiDotNetCore/Abstractions/JsonApiContext.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace JsonApiDotNetCore.Abstractions
99
{
10-
public class JsonApiContext
10+
public class JsonApiContext : IJsonApiContext
1111
{
1212
public HttpContext HttpContext { get; }
1313
public Route Route { get; }
@@ -24,7 +24,7 @@ public JsonApiContext(HttpContext httpContext, Route route, object dbContext, Js
2424

2525
public Type GetJsonApiResourceType()
2626
{
27-
return Configuration.ResourceMapDefinitions[Route.BaseModelType];
27+
return Configuration.ResourceMapDefinitions[Route.BaseModelType].Item1;
2828
}
2929

3030
public string GetEntityName()
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
using System;
22
using System.Collections.Generic;
33
using AutoMapper;
4+
using JsonApiDotNetCore.Abstractions;
45

56
namespace JsonApiDotNetCore.Configuration
67
{
78
public interface IJsonApiModelConfiguration
89
{
10+
/// <summary>
11+
/// The database context to use
12+
/// </summary>
13+
/// <typeparam name="T"></typeparam>
14+
/// <exception cref="ArgumentException"></exception>
915
void UseContext<T>();
16+
17+
/// <summary>
18+
/// The request namespace.
19+
/// </summary>
20+
/// <param name="ns"></param>
21+
/// <example>api/v1</example>
1022
void SetDefaultNamespace(string ns);
11-
void DefineResourceMapping(Action<Dictionary<Type,Type>> mapping);
23+
24+
/// <summary>
25+
/// Define explicit mapping of a model to a class that implements IJsonApiResource
26+
/// </summary>
27+
/// <typeparam name="TModel"></typeparam>
28+
/// <typeparam name="TResource"></typeparam>
29+
/// <param name="mappingExpression"></param>
30+
/// <exception cref="ArgumentException"></exception>
31+
void AddResourceMapping<TModel, TResource>(Action<IMappingExpression> mappingExpression);
32+
33+
/// <summary>
34+
/// Specifies a controller override class for a particular model type.
35+
/// </summary>
36+
/// <typeparam name="TModel"></typeparam>
37+
/// <typeparam name="TController"></typeparam>
38+
/// <exception cref="ArgumentException"></exception>
39+
void UseController<TModel, TController>();
1240
}
1341
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Reflection;
3+
using JsonApiDotNetCore.Routing;
4+
using Microsoft.EntityFrameworkCore;
5+
using System.Linq;
6+
using System.Linq.Expressions;
7+
using AutoMapper;
8+
using JsonApiDotNetCore.Abstractions;
9+
using JsonApiDotNetCore.Attributes;
10+
11+
namespace JsonApiDotNetCore.Configuration
12+
{
13+
public class JsonApiConfigurationBuilder
14+
{
15+
private readonly Action<IJsonApiModelConfiguration> _configurationAction;
16+
private JsonApiModelConfiguration Config { get; set; }
17+
18+
public JsonApiConfigurationBuilder(Action<IJsonApiModelConfiguration> configurationAction)
19+
{
20+
Config = new JsonApiModelConfiguration();
21+
_configurationAction = configurationAction;
22+
}
23+
24+
public JsonApiModelConfiguration Build()
25+
{
26+
Config = new JsonApiModelConfiguration();
27+
_configurationAction.Invoke(Config);
28+
CheckIsValidConfiguration();
29+
LoadModelRoutesFromContext();
30+
SetupResourceMaps();
31+
return Config;
32+
}
33+
34+
private void CheckIsValidConfiguration()
35+
{
36+
if (Config.ContextType == null)
37+
throw new NullReferenceException("DbContext is not specified");
38+
}
39+
40+
private void LoadModelRoutesFromContext()
41+
{
42+
// Assumption: all DbSet<> types should be included in the route list
43+
var properties = Config.ContextType.GetProperties().ToList();
44+
45+
properties.ForEach(property =>
46+
{
47+
if (property.PropertyType.GetTypeInfo().IsGenericType &&
48+
property.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
49+
{
50+
51+
var modelType = property.PropertyType.GetGenericArguments()[0];
52+
53+
var route = new RouteDefinition
54+
{
55+
ModelType = modelType,
56+
PathString = RouteBuilder.BuildRoute(Config.Namespace, property.Name),
57+
ContextPropertyName = property.Name
58+
};
59+
60+
Config.Routes.Add(route);
61+
}
62+
});
63+
}
64+
65+
private void SetupResourceMaps()
66+
{
67+
LoadDefaultResourceMaps();
68+
var mapConfiguration = new MapperConfiguration(cfg =>
69+
{
70+
foreach (var definition in Config.ResourceMapDefinitions)
71+
{
72+
var mappingExpression = cfg.CreateMap(definition.Key, definition.Value.Item1);
73+
definition.Value.Item2?.Invoke(mappingExpression);
74+
}
75+
});
76+
Config.ResourceMapper = mapConfiguration.CreateMapper();
77+
}
78+
79+
private void LoadDefaultResourceMaps()
80+
{
81+
var resourceAttribute = typeof(JsonApiResourceAttribute);
82+
var modelTypes = Assembly.GetEntryAssembly().DefinedTypes.Where(t => t.GetCustomAttributes(resourceAttribute).Count() == 1);
83+
84+
foreach (var modelType in modelTypes)
85+
{
86+
var resourceType = ((JsonApiResourceAttribute)modelType.GetCustomAttribute(resourceAttribute)).JsonApiResourceType;
87+
88+
// do not overwrite custom definitions
89+
if(!Config.ResourceMapDefinitions.ContainsKey(modelType.UnderlyingSystemType))
90+
{
91+
Config.ResourceMapDefinitions.Add(modelType.UnderlyingSystemType, new Tuple<Type, Action<IMappingExpression>>(resourceType, null));
92+
}
93+
}
94+
}
95+
}
96+
}

JsonApiDotNetCore/Configuration/JsonApiModelConfiguration.cs

+22-37
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.Linq;
44
using System.Reflection;
55
using AutoMapper;
6+
using JsonApiDotNetCore.Abstractions;
7+
using JsonApiDotNetCore.Controllers;
68
using JsonApiDotNetCore.JsonApi;
79
using JsonApiDotNetCore.Routing;
810
using Microsoft.AspNetCore.Http;
@@ -16,59 +18,42 @@ public class JsonApiModelConfiguration : IJsonApiModelConfiguration
1618
public IMapper ResourceMapper;
1719
public Type ContextType { get; set; }
1820
public List<RouteDefinition> Routes = new List<RouteDefinition>();
19-
public Dictionary<Type, Type> ResourceMapDefinitions = new Dictionary<Type, Type>();
21+
public Dictionary<Type, Tuple<Type, Action<IMappingExpression>>> ResourceMapDefinitions = new Dictionary<Type, Tuple<Type, Action<IMappingExpression>>>();
22+
public Dictionary<Type, Type> ControllerOverrides = new Dictionary<Type, Type>();
2023

2124
public void SetDefaultNamespace(string ns)
2225
{
2326
Namespace = ns;
2427
}
2528

26-
public void DefineResourceMapping(Action<Dictionary<Type,Type>> mapping)
29+
public void AddResourceMapping<TModel, TResource>(Action<IMappingExpression> mappingExpression)
2730
{
28-
mapping.Invoke(ResourceMapDefinitions);
31+
var resourceType = typeof(TResource);
32+
var modelType = typeof(TModel);
2933

30-
var mapConfiguration = new MapperConfiguration(cfg =>
31-
{
32-
foreach (var definition in ResourceMapDefinitions)
33-
{
34-
cfg.CreateMap(definition.Key, definition.Value);
35-
}
36-
});
34+
if (!resourceType.GetInterfaces().Contains(typeof(IJsonApiResource)))
35+
throw new ArgumentException("Specified type does not implement IJsonApiResource", nameof(resourceType));
3736

38-
ResourceMapper = mapConfiguration.CreateMapper();
37+
ResourceMapDefinitions.Add(modelType, new Tuple<Type, Action<IMappingExpression>>(resourceType, mappingExpression));
3938
}
4039

41-
public void UseContext<T>()
42-
{
43-
// TODO: assert the context is of type DbContext
44-
ContextType = typeof(T);
45-
LoadModelRoutesFromContext();
46-
}
47-
48-
private void LoadModelRoutesFromContext()
40+
public void UseController<TModel, TController>()
4941
{
50-
// Assumption: all DbSet<> types should be included in the route list
51-
var properties = ContextType.GetProperties().ToList();
42+
var modelType = typeof(TModel);
43+
var controllerType = typeof(TController);
5244

53-
properties.ForEach(property =>
54-
{
55-
if (property.PropertyType.GetTypeInfo().IsGenericType &&
56-
property.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
57-
{
45+
if (!controllerType.GetInterfaces().Contains(typeof(IJsonApiController)))
46+
throw new ArgumentException("Specified type does not implement IJsonApiController", nameof(controllerType));
5847

59-
var modelType = property.PropertyType.GetGenericArguments()[0];
48+
ControllerOverrides[modelType] = controllerType;
49+
}
6050

61-
var route = new RouteDefinition
62-
{
63-
ModelType = modelType,
64-
PathString = RouteBuilder.BuildRoute(Namespace, property.Name),
65-
ContextPropertyName = property.Name
66-
};
51+
public void UseContext<T>()
52+
{
53+
ContextType = typeof(T);
6754

68-
Routes.Add(route);
69-
}
70-
});
55+
if (!typeof(DbContext).IsAssignableFrom(ContextType))
56+
throw new ArgumentException("Context Type must derive from DbContext", nameof(T));
7157
}
72-
7358
}
7459
}

0 commit comments

Comments
 (0)