Skip to content

Commit d4cb99d

Browse files
authored
Merge pull request #89 from Research-Institute/feat/decouple-dal
De-Couple DAL from Entity Framework
2 parents 581f4bd + 8d134f3 commit d4cb99d

Some content is hidden

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

47 files changed

+1798
-346
lines changed

JsonApiDotnetCore.sln

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
21
Microsoft Visual Studio Solution File, Format Version 12.00
32
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26228.4
3+
VisualStudioVersion = 15.0.26228.9
54
MinimumVisualStudioVersion = 10.0.40219.1
65
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{C0EC9E70-EB2E-436F-9D94-FA16FA774123}"
76
EndProject
@@ -18,24 +17,68 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1817
.gitignore = .gitignore
1918
EndProjectSection
2019
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkExample", "src\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{570165EC-62B5-4684-A139-8D2A30DD4475}"
21+
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{73DA578D-A63F-4956-83ED-6D7102E09140}"
23+
EndProject
2124
Global
2225
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2326
Debug|Any CPU = Debug|Any CPU
27+
Debug|x64 = Debug|x64
28+
Debug|x86 = Debug|x86
2429
Release|Any CPU = Release|Any CPU
30+
Release|x64 = Release|x64
31+
Release|x86 = Release|x86
2532
EndGlobalSection
2633
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2734
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2835
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|Any CPU.Build.0 = Debug|Any CPU
36+
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|x64.ActiveCfg = Debug|Any CPU
37+
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|x86.ActiveCfg = Debug|Any CPU
2938
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|Any CPU.ActiveCfg = Release|Any CPU
3039
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|Any CPU.Build.0 = Release|Any CPU
40+
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|x64.ActiveCfg = Release|Any CPU
41+
{C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|x86.ActiveCfg = Release|Any CPU
3142
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3243
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|Any CPU.Build.0 = Debug|Any CPU
44+
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|x64.ActiveCfg = Debug|Any CPU
45+
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|x86.ActiveCfg = Debug|Any CPU
3346
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|Any CPU.ActiveCfg = Release|Any CPU
3447
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|Any CPU.Build.0 = Release|Any CPU
48+
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|x64.ActiveCfg = Release|Any CPU
49+
{97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|x86.ActiveCfg = Release|Any CPU
3550
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3651
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
52+
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|x64.ActiveCfg = Debug|Any CPU
53+
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|x86.ActiveCfg = Debug|Any CPU
3754
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
3855
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|Any CPU.Build.0 = Release|Any CPU
56+
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|x64.ActiveCfg = Release|Any CPU
57+
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|x86.ActiveCfg = Release|Any CPU
58+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|Any CPU.Build.0 = Debug|Any CPU
60+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x64.ActiveCfg = Debug|Any CPU
61+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x64.Build.0 = Debug|Any CPU
62+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x86.ActiveCfg = Debug|Any CPU
63+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x86.Build.0 = Debug|Any CPU
64+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|Any CPU.ActiveCfg = Release|Any CPU
65+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|Any CPU.Build.0 = Release|Any CPU
66+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x64.ActiveCfg = Release|Any CPU
67+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x64.Build.0 = Release|Any CPU
68+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x86.ActiveCfg = Release|Any CPU
69+
{570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x86.Build.0 = Release|Any CPU
70+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|Any CPU.Build.0 = Debug|Any CPU
72+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x64.ActiveCfg = Debug|Any CPU
73+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x64.Build.0 = Debug|Any CPU
74+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x86.ActiveCfg = Debug|Any CPU
75+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x86.Build.0 = Debug|Any CPU
76+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|Any CPU.ActiveCfg = Release|Any CPU
77+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|Any CPU.Build.0 = Release|Any CPU
78+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x64.ActiveCfg = Release|Any CPU
79+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x64.Build.0 = Release|Any CPU
80+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x86.ActiveCfg = Release|Any CPU
81+
{73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x86.Build.0 = Release|Any CPU
3982
EndGlobalSection
4083
GlobalSection(SolutionProperties) = preSolution
4184
HideSolutionNode = FALSE
@@ -44,5 +87,7 @@ Global
4487
{C0EC9E70-EB2E-436F-9D94-FA16FA774123} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
4588
{97EE048B-16C0-43F6-BDA9-4E762B2F579F} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
4689
{0B959765-40D2-43B5-87EE-FE2FEF9DBED5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
90+
{570165EC-62B5-4684-A139-8D2A30DD4475} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
91+
{73DA578D-A63F-4956-83ED-6D7102E09140} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
4792
EndGlobalSection
4893
EndGlobal

README.md

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or
1717
- [Defining Models](#defining-models)
1818
- [Specifying Public Attributes](#specifying-public-attributes)
1919
- [Relationships](#relationships)
20+
- [Resource Names](#resource-names)
2021
- [Defining Controllers](#defining-controllers)
2122
- [Non-Integer Type Keys](#non-integer-type-keys)
2223
- [Routing](#routing)
@@ -169,6 +170,23 @@ public class TodoItem : Identifiable<int>
169170
}
170171
```
171172

173+
#### Resource Names
174+
175+
If a DbContext is specified when adding the services, the context will be used to define the resources and their names.
176+
177+
```csharp
178+
public DbSet<MyModel> SomeModels { get; set; } // this will be translated into "some-models"
179+
```
180+
181+
However, you can specify a custom name like so:
182+
183+
```csharp
184+
[Resource("some-models")]
185+
public DbSet<MyModel> MyModels { get; set; } // this will be translated into "some-models"
186+
```
187+
188+
For further resource customizations, please see the section on [Defining Custom Data Access Methods](#defining-custom-data-access-methods).
189+
172190
### Defining Controllers
173191

174192
You need to create controllers that inherit from `JsonApiController<TEntity>` or `JsonApiController<TEntity, TId>`
@@ -180,9 +198,9 @@ public class ThingsController : JsonApiController<Thing>
180198
{
181199
public ThingsController(
182200
IJsonApiContext jsonApiContext,
183-
IEntityRepository<Thing> entityRepository,
201+
IResourceService<Thing> resourceService,
184202
ILoggerFactory loggerFactory)
185-
: base(jsonApiContext, entityRepository, loggerFactory)
203+
: base(jsonApiContext, resourceService, loggerFactory)
186204
{ }
187205
}
188206
```
@@ -199,9 +217,9 @@ public class ThingsController : JsonApiController<Thing, Guid>
199217
{
200218
public ThingsController(
201219
IJsonApiContext jsonApiContext,
202-
IEntityRepository<Thing, Guid> entityRepository,
220+
IResourceService<Thing, Guid> resourceService,
203221
ILoggerFactory loggerFactory)
204-
: base(jsonApiContext, entityRepository, loggerFactory)
222+
: base(jsonApiContext, resourceService, loggerFactory)
205223
{ }
206224
}
207225
```
@@ -228,7 +246,67 @@ services.AddJsonApi<AppDbContext>(
228246

229247
### Defining Custom Data Access Methods
230248

231-
You can implement custom methods for accessing the data by creating an implementation of
249+
By default, data retrieval is distributed across 3 layers:
250+
251+
1. `JsonApiController`
252+
2. `EntityResourceService`
253+
3. `DefaultEntityRepository`
254+
255+
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.
256+
257+
#### Not Using Entity Framework?
258+
259+
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:
260+
261+
```csharp
262+
// Startup.cs
263+
public void ConfigureServices(IServiceCollection services)
264+
{
265+
// Add framework services.
266+
var mvcBuilder = services.AddMvc();
267+
268+
services.AddJsonApi(options => {
269+
options.Namespace = "api/v1";
270+
options.BuildContextGraph((builder) => {
271+
builder.AddResource<MyModel>("my-models");
272+
});
273+
}, mvcBuilder);
274+
// ...
275+
}
276+
```
277+
278+
#### Custom Resource Service Implementation
279+
280+
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:
281+
282+
```csharp
283+
// Startup.cs
284+
public void ConfigureServices(IServiceCollection services)
285+
{
286+
services.AddScoped<IResourceService<MyModel>, MyModelService>();
287+
// ...
288+
}
289+
```
290+
291+
```csharp
292+
// MyModelService.cs
293+
public class MyModelService : IResourceService<MyModel>
294+
{
295+
private readonly IMyModelDAL _dal;
296+
public MyModelService(IMyModelDAL dal)
297+
{
298+
_dal = dal;
299+
}
300+
public Task<IEnumerable<MyModel>> GetAsync()
301+
{
302+
return await _dal.GetModelAsync();
303+
}
304+
}
305+
```
306+
307+
#### Custom Entity Repository Implementation
308+
309+
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
232310
`IEntityRepository<TEntity, TId>`. If you only need minor changes you can override the
233311
methods defined in `DefaultEntityRepository<TEntity, TId>`. The repository should then be
234312
add to the service collection in `Startup.ConfigureServices` like so:

build.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ set -e
66
dotnet restore ./src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
77
dotnet restore ./src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
88
dotnet restore ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
9+
dotnet restore ./test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj
910

10-
dotnet test ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
11+
dotnet test ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
12+
dotnet test ./test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using Microsoft.EntityFrameworkCore;
5+
using JsonApiDotNetCore.Internal;
6+
using JsonApiDotNetCore.Models;
7+
using JsonApiDotNetCore.Extensions;
8+
9+
namespace JsonApiDotNetCore.Builders
10+
{
11+
public class ContextGraphBuilder : IContextGraphBuilder
12+
{
13+
private List<ContextEntity> Entities;
14+
private bool _usesDbContext;
15+
public ContextGraphBuilder()
16+
{
17+
Entities = new List<ContextEntity>();
18+
}
19+
20+
public IContextGraph Build()
21+
{
22+
var graph = new ContextGraph()
23+
{
24+
Entities = Entities,
25+
UsesDbContext = _usesDbContext
26+
};
27+
return graph;
28+
}
29+
30+
public void AddResource<TResource>(string pluralizedTypeName) where TResource : class
31+
{
32+
var entityType = typeof(TResource);
33+
Entities.Add(new ContextEntity
34+
{
35+
EntityName = pluralizedTypeName,
36+
EntityType = entityType,
37+
Attributes = GetAttributes(entityType),
38+
Relationships = GetRelationships(entityType)
39+
});
40+
}
41+
42+
protected virtual List<AttrAttribute> GetAttributes(Type entityType)
43+
{
44+
var attributes = new List<AttrAttribute>();
45+
46+
var properties = entityType.GetProperties();
47+
48+
foreach (var prop in properties)
49+
{
50+
var attribute = (AttrAttribute)prop.GetCustomAttribute(typeof(AttrAttribute));
51+
if (attribute == null) continue;
52+
attribute.InternalAttributeName = prop.Name;
53+
attributes.Add(attribute);
54+
}
55+
return attributes;
56+
}
57+
58+
protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
59+
{
60+
var attributes = new List<RelationshipAttribute>();
61+
62+
var properties = entityType.GetProperties();
63+
64+
foreach (var prop in properties)
65+
{
66+
var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute));
67+
if (attribute == null) continue;
68+
attribute.InternalRelationshipName = prop.Name;
69+
attribute.Type = GetRelationshipType(attribute, prop);
70+
attributes.Add(attribute);
71+
}
72+
return attributes;
73+
}
74+
75+
protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop)
76+
{
77+
if (relation.IsHasMany)
78+
return prop.PropertyType.GetGenericArguments()[0];
79+
else
80+
return prop.PropertyType;
81+
}
82+
83+
public void AddDbContext<T>() where T : DbContext
84+
{
85+
_usesDbContext = true;
86+
87+
var contextType = typeof(T);
88+
89+
var entities = new List<ContextEntity>();
90+
91+
var contextProperties = contextType.GetProperties();
92+
93+
foreach (var property in contextProperties)
94+
{
95+
var dbSetType = property.PropertyType;
96+
97+
if (dbSetType.GetTypeInfo().IsGenericType
98+
&& dbSetType.GetGenericTypeDefinition() == typeof(DbSet<>))
99+
{
100+
var entityType = dbSetType.GetGenericArguments()[0];
101+
entities.Add(new ContextEntity
102+
{
103+
EntityName = GetResourceName(property),
104+
EntityType = entityType,
105+
Attributes = GetAttributes(entityType),
106+
Relationships = GetRelationships(entityType)
107+
});
108+
}
109+
}
110+
111+
Entities = entities;
112+
}
113+
114+
private string GetResourceName(PropertyInfo property)
115+
{
116+
var resourceAttribute = property.GetCustomAttribute(typeof(ResourceAttribute));
117+
if(resourceAttribute == null)
118+
return property.Name.Dasherize();
119+
120+
return ((ResourceAttribute)resourceAttribute).ResourceName;
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)