Skip to content

De-Couple DAL from Entity Framework #89

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

Merged
merged 36 commits into from
Apr 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4ed46c8
feat(services): define IResourceService
jaredcnance Apr 8, 2017
fa9364f
refactor(controller): move bulk of logic into the resource service
jaredcnance Apr 8, 2017
b62227d
feat(service-collection-ext): add new service
jaredcnance Apr 8, 2017
6d6c3e4
chore(example-controllers): update for new JsonApiController ctor
jaredcnance Apr 8, 2017
e17cad2
docs(readme): stub out documentation updates
jaredcnance Apr 8, 2017
550e00a
feat(context-graph): first pass at refactoring the graph API
jaredcnance Apr 12, 2017
ba4719d
fix(IContextGraphBuilder): expose the AddDbContext method
jaredcnance Apr 13, 2017
b89b18f
refactor(service-collection-ext): prevent breaking changes
jaredcnance Apr 13, 2017
4b859a3
hack(service-collection-ext): inject bogus dbContext types for DI
jaredcnance Apr 13, 2017
8457b22
example: add "no entity framework" example
jaredcnance Apr 13, 2017
99e8b4d
test(extensibility): add initial test around fetching non-ef data
jaredcnance Apr 13, 2017
194d31d
Merge remote-tracking branch 'origin/develop' into feat/decouple-dal
jaredcnance Apr 13, 2017
09cc32a
example(no-ef): rename models, controllers seem to cause conflict
jaredcnance Apr 13, 2017
7c20d77
refactor(context-graph): the stored entity name should be dasherized …
jaredcnance Apr 14, 2017
2fd9eb3
test(no-ef): validate the response body
jaredcnance Apr 14, 2017
94e5dee
docs(readme): document custom data implementation
jaredcnance Apr 14, 2017
e6ba54f
docs(readme): document custom context mapping
jaredcnance Apr 14, 2017
0264ea8
docs(readme): document new controller constructor
jaredcnance Apr 14, 2017
84cf741
feat(example): use Dapper and Npgsql in NoEF example
jaredcnance Apr 18, 2017
93edba5
test(acceptance/no-ef): ensure the database is created
jaredcnance Apr 18, 2017
2c164fa
test(acceptance/no-ef): ensure database is migrated
jaredcnance Apr 18, 2017
edc4626
fix(example/startups): try using EnsureCreated to fix travis-ci build
jaredcnance Apr 18, 2017
7c14d49
refactor(tests): split test projects
jaredcnance Apr 18, 2017
36b55d8
fix(ci): restore new test project
jaredcnance Apr 19, 2017
62183f3
chore(example): remove build files from source control
jaredcnance Apr 19, 2017
629169f
chore(csproj): bump package major version
jaredcnance Apr 19, 2017
fd5e04a
feat(context-graph-builder): intro resource attr
jaredcnance Apr 19, 2017
b0146c4
refactor(*): further reduction of Dasherize usage
jaredcnance Apr 19, 2017
75ae1b5
docs(readme): show ResourceAttribute usage
jaredcnance Apr 19, 2017
3d95c01
fix(de-serializer): do not dasherize a public name
jaredcnance Apr 19, 2017
7cff6d0
test(no-ef): additional tests / examples
jaredcnance Apr 19, 2017
517367b
fix(example): missing attribute
jaredcnance Apr 19, 2017
aec7e1c
test(extensibility): add tests for dasherized routes for inherited an…
JanMattner Apr 20, 2017
4480e6b
fix(link-builder): do not dasherize routes by default
jaredcnance Apr 21, 2017
1de23d8
test(extensibility): fix/remove failing tests
jaredcnance Apr 25, 2017
8d134f3
chore(csproj): add package information
jaredcnance Apr 25, 2017
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
49 changes: 47 additions & 2 deletions JsonApiDotnetCore.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26228.4
VisualStudioVersion = 15.0.26228.9
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{C0EC9E70-EB2E-436F-9D94-FA16FA774123}"
EndProject
Expand All @@ -18,24 +17,68 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.gitignore = .gitignore
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkExample", "src\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{570165EC-62B5-4684-A139-8D2A30DD4475}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{73DA578D-A63F-4956-83ED-6D7102E09140}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|x64.ActiveCfg = Debug|Any CPU
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|x86.ActiveCfg = Debug|Any CPU
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|Any CPU.Build.0 = Release|Any CPU
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|x64.ActiveCfg = Release|Any CPU
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|x86.ActiveCfg = Release|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|x64.ActiveCfg = Debug|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|x86.ActiveCfg = Debug|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|Any CPU.Build.0 = Release|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|x64.ActiveCfg = Release|Any CPU
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|x86.ActiveCfg = Release|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|x64.ActiveCfg = Debug|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|x86.ActiveCfg = Debug|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|Any CPU.Build.0 = Release|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|x64.ActiveCfg = Release|Any CPU
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|x86.ActiveCfg = Release|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|Any CPU.Build.0 = Debug|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x64.ActiveCfg = Debug|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x64.Build.0 = Debug|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x86.ActiveCfg = Debug|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x86.Build.0 = Debug|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|Any CPU.ActiveCfg = Release|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|Any CPU.Build.0 = Release|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x64.ActiveCfg = Release|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x64.Build.0 = Release|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x86.ActiveCfg = Release|Any CPU
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x86.Build.0 = Release|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x64.ActiveCfg = Debug|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x64.Build.0 = Debug|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x86.ActiveCfg = Debug|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x86.Build.0 = Debug|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|Any CPU.Build.0 = Release|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x64.ActiveCfg = Release|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x64.Build.0 = Release|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x86.ActiveCfg = Release|Any CPU
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -44,5 +87,7 @@ Global
{C0EC9E70-EB2E-436F-9D94-FA16FA774123} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{97EE048B-16C0-43F6-BDA9-4E762B2F579F} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{570165EC-62B5-4684-A139-8D2A30DD4475} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{73DA578D-A63F-4956-83ED-6D7102E09140} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
EndGlobalSection
EndGlobal
88 changes: 83 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or
- [Defining Models](#defining-models)
- [Specifying Public Attributes](#specifying-public-attributes)
- [Relationships](#relationships)
- [Resource Names](#resource-names)
- [Defining Controllers](#defining-controllers)
- [Non-Integer Type Keys](#non-integer-type-keys)
- [Routing](#routing)
Expand Down Expand Up @@ -169,6 +170,23 @@ public class TodoItem : Identifiable<int>
}
```

#### Resource Names

If a DbContext is specified when adding the services, the context will be used to define the resources and their names.

```csharp
public DbSet<MyModel> SomeModels { get; set; } // this will be translated into "some-models"
```

However, you can specify a custom name like so:

```csharp
[Resource("some-models")]
public DbSet<MyModel> MyModels { get; set; } // this will be translated into "some-models"
```

For further resource customizations, please see the section on [Defining Custom Data Access Methods](#defining-custom-data-access-methods).

### Defining Controllers

You need to create controllers that inherit from `JsonApiController<TEntity>` or `JsonApiController<TEntity, TId>`
Expand All @@ -180,9 +198,9 @@ public class ThingsController : JsonApiController<Thing>
{
public ThingsController(
IJsonApiContext jsonApiContext,
IEntityRepository<Thing> entityRepository,
IResourceService<Thing> resourceService,
ILoggerFactory loggerFactory)
: base(jsonApiContext, entityRepository, loggerFactory)
: base(jsonApiContext, resourceService, loggerFactory)
{ }
}
```
Expand All @@ -199,9 +217,9 @@ public class ThingsController : JsonApiController<Thing, Guid>
{
public ThingsController(
IJsonApiContext jsonApiContext,
IEntityRepository<Thing, Guid> entityRepository,
IResourceService<Thing, Guid> resourceService,
ILoggerFactory loggerFactory)
: base(jsonApiContext, entityRepository, loggerFactory)
: base(jsonApiContext, resourceService, loggerFactory)
{ }
}
```
Expand All @@ -228,7 +246,67 @@ services.AddJsonApi<AppDbContext>(

### Defining Custom Data Access Methods

You can implement custom methods for accessing the data by creating an implementation of
By default, data retrieval is distributed across 3 layers:

1. `JsonApiController`
2. `EntityResourceService`
3. `DefaultEntityRepository`

Customization can be done at any of these layers. However, it is recommended that you make your customizations at the service or the repository layer when possible to keep the controllers free of unnecessary logic.

#### Not Using Entity Framework?

Out of the box, the library uses your `DbContext` to create a "ContextGraph" or map of all your models and their relationships. If, however, you have models that are not members of a `DbContext`, you can manually create this graph like so:

```csharp
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
var mvcBuilder = services.AddMvc();

services.AddJsonApi(options => {
options.Namespace = "api/v1";
options.BuildContextGraph((builder) => {
builder.AddResource<MyModel>("my-models");
});
}, mvcBuilder);
// ...
}
```

#### Custom Resource Service Implementation

By default, this library uses Entity Framework. If you'd like to use another ORM that does not implement `IQueryable`, you can inject a custom service like so:

```csharp
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IResourceService<MyModel>, MyModelService>();
// ...
}
```

```csharp
// MyModelService.cs
public class MyModelService : IResourceService<MyModel>
{
private readonly IMyModelDAL _dal;
public MyModelService(IMyModelDAL dal)
{
_dal = dal;
}
public Task<IEnumerable<MyModel>> GetAsync()
{
return await _dal.GetModelAsync();
}
}
```

#### Custom Entity Repository Implementation

If you want to use EF, but need additional data access logic (such as authorization), you can implement custom methods for accessing the data by creating an implementation of
`IEntityRepository<TEntity, TId>`. If you only need minor changes you can override the
methods defined in `DefaultEntityRepository<TEntity, TId>`. The repository should then be
add to the service collection in `Startup.ConfigureServices` like so:
Expand Down
4 changes: 3 additions & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ set -e
dotnet restore ./src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
dotnet restore ./src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
dotnet restore ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
dotnet restore ./test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj

dotnet test ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
dotnet test ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
dotnet test ./test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj
123 changes: 123 additions & 0 deletions src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Extensions;

namespace JsonApiDotNetCore.Builders
{
public class ContextGraphBuilder : IContextGraphBuilder
{
private List<ContextEntity> Entities;
private bool _usesDbContext;
public ContextGraphBuilder()
{
Entities = new List<ContextEntity>();
}

public IContextGraph Build()
{
var graph = new ContextGraph()
{
Entities = Entities,
UsesDbContext = _usesDbContext
};
return graph;
}

public void AddResource<TResource>(string pluralizedTypeName) where TResource : class
{
var entityType = typeof(TResource);
Entities.Add(new ContextEntity
{
EntityName = pluralizedTypeName,
Copy link
Contributor

Choose a reason for hiding this comment

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

This name is not dasherized. Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See comment below. I'd like to avoid mutating any manual input so we can avoid inconsistencies as described in #93

EntityType = entityType,
Attributes = GetAttributes(entityType),
Relationships = GetRelationships(entityType)
});
}

protected virtual List<AttrAttribute> GetAttributes(Type entityType)
{
var attributes = new List<AttrAttribute>();

var properties = entityType.GetProperties();

foreach (var prop in properties)
{
var attribute = (AttrAttribute)prop.GetCustomAttribute(typeof(AttrAttribute));
if (attribute == null) continue;
attribute.InternalAttributeName = prop.Name;
attributes.Add(attribute);
}
return attributes;
}

protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
{
var attributes = new List<RelationshipAttribute>();

var properties = entityType.GetProperties();

foreach (var prop in properties)
{
var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute));
if (attribute == null) continue;
attribute.InternalRelationshipName = prop.Name;
attribute.Type = GetRelationshipType(attribute, prop);
attributes.Add(attribute);
}
return attributes;
}

protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop)
{
if (relation.IsHasMany)
return prop.PropertyType.GetGenericArguments()[0];
else
return prop.PropertyType;
}

public void AddDbContext<T>() where T : DbContext
{
_usesDbContext = true;

var contextType = typeof(T);

var entities = new List<ContextEntity>();

var contextProperties = contextType.GetProperties();

foreach (var property in contextProperties)
{
var dbSetType = property.PropertyType;

if (dbSetType.GetTypeInfo().IsGenericType
&& dbSetType.GetGenericTypeDefinition() == typeof(DbSet<>))
{
var entityType = dbSetType.GetGenericArguments()[0];
entities.Add(new ContextEntity
{
EntityName = GetResourceName(property),
EntityType = entityType,
Attributes = GetAttributes(entityType),
Relationships = GetRelationships(entityType)
});
}
}

Entities = entities;
}

private string GetResourceName(PropertyInfo property)
{
var resourceAttribute = property.GetCustomAttribute(typeof(ResourceAttribute));
if(resourceAttribute == null)
return property.Name.Dasherize();

return ((ResourceAttribute)resourceAttribute).ResourceName;
}
}
}
Loading