├── .editorconfig
├── .github
└── workflows
│ └── dotnet.yml
├── .gitignore
├── Directory.Build.props
├── LICENSE
├── README.md
├── SystemTextJson.FluentApi.sln
├── samples
└── SystemTextJson.FluentApi.QuickStart
│ ├── Program.cs
│ └── SystemTextJson.FluentApi.QuickStart.csproj
├── src
└── SystemTextJson.FluentApi
│ ├── CustomEnumConverter.cs
│ ├── CustomizableJsonStringEnumConverter.cs
│ ├── EntityTypeBuilder.cs
│ ├── EntityTypeBuilderExtensions.cs
│ ├── IEntityTypeBuilder.cs
│ ├── IHaveChangedProperties.cs
│ ├── IMemberPropertyBuilder.cs
│ ├── IPropertyBuilder.cs
│ ├── InlineArrayJsonConverter.cs
│ ├── JsonModelBuilder.cs
│ ├── JsonPropertyInfoExtensions.cs
│ ├── JsonSerializerOptionsExtensions.cs
│ ├── JsonTypeInfoResolverExtensions.cs
│ ├── MemberPropertyBuilder.cs
│ ├── ObjectSerializer.cs
│ ├── PropertyBuilderExtensions.cs
│ ├── SerializationHelpers.cs
│ ├── SystemTextJson.FluentApi.csproj
│ ├── ValueTupleJsonConverter.cs
│ └── VirtualPropertyBuilder.cs
└── tests
└── SystemTextJson.FluentApi.Tests
├── CustomizableJsonStringEnumConverterTests.cs
├── EntityTypeBuilderTests.cs
├── InlineArrayJsonConverterTests.cs
├── JsonAsserts.cs
├── JsonModelBuilderTests.cs
├── PropertyBuilderTests.cs
├── SystemTextJson.FluentApi.Tests.csproj
└── ValueTupleJsonConverterTests.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome:http://EditorConfig.org
2 | # from https://github.com/dotnet/roslyn/blob/master/.editorconfig
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Don't use tabs for indentation.
7 | [*]
8 | indent_style = space
9 | # (Please don't specify an indent_size here; that has too many unintended consequences.)
10 |
11 | # Code files
12 | [*.cs]
13 | indent_size = 4
14 | insert_final_newline = true
15 | charset = utf-8
16 | max_line_length = 120
17 |
18 | # Xml project files
19 | [*.csproj]
20 | indent_size = 2
21 | charset = utf-8
22 |
23 | # Xml config files
24 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
25 | indent_size = 2
26 |
27 | # JSON files
28 | [*.json]
29 | indent_size = 2
30 |
31 | [*.{sh}]
32 | end_of_line = lf
33 | indent_size = 2
34 |
35 | # Dotnet code style settings:
36 | [*.{cs,vb}]
37 | # Sort using and Import directives with System.* appearing first
38 | dotnet_sort_system_directives_first = true
39 | # Avoid "this." and "Me." if not necessary
40 | dotnet_style_qualification_for_field = false:suggestion
41 | dotnet_style_qualification_for_property = false:suggestion
42 | dotnet_style_qualification_for_method = false:suggestion
43 | dotnet_style_qualification_for_event = false:suggestion
44 |
45 | # Use language keywords instead of framework type names for type references
46 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
47 | dotnet_style_predefined_type_for_member_access = true:suggestion
48 |
49 | # Suggest more modern language features when available
50 | dotnet_style_object_initializer = true:suggestion
51 | dotnet_style_collection_initializer = true:suggestion
52 | dotnet_style_coalesce_expression = true:suggestion
53 | dotnet_style_null_propagation = true:suggestion
54 | dotnet_style_explicit_tuple_names = true:suggestion
55 |
56 | # Non-private static fields are PascalCase
57 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
58 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
59 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
60 |
61 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
62 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
63 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
64 |
65 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
66 |
67 | # Constants are PascalCase
68 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
69 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
70 | dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style
71 |
72 | dotnet_naming_symbols.constants.applicable_kinds = field, local
73 | dotnet_naming_symbols.constants.required_modifiers = const
74 |
75 | dotnet_naming_style.constant_style.capitalization = pascal_case
76 |
77 | # Static fields are camelCase and start with s_
78 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
79 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
80 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
81 |
82 | dotnet_naming_symbols.static_fields.applicable_kinds = field
83 | dotnet_naming_symbols.static_fields.required_modifiers = static
84 | dotnet_naming_symbols.static_fields.applicable_accessibilities = private
85 |
86 | dotnet_naming_style.static_field_style.capitalization = camel_case
87 | dotnet_naming_style.static_field_style.required_prefix = s_
88 |
89 | # Instance fields are camelCase and start with _
90 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
91 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
92 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
93 |
94 | dotnet_naming_symbols.instance_fields.applicable_kinds = field
95 | dotnet_naming_symbols.instance_fields.applicable_accessibilities = private
96 |
97 | dotnet_naming_style.instance_field_style.capitalization = camel_case
98 | dotnet_naming_style.instance_field_style.required_prefix = _
99 |
100 | # Locals and parameters are camelCase
101 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
102 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
103 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
104 |
105 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
106 |
107 | dotnet_naming_style.camel_case_style.capitalization = camel_case
108 |
109 | # Local functions are PascalCase
110 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
111 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
112 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style
113 |
114 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function
115 |
116 | dotnet_naming_style.local_function_style.capitalization = pascal_case
117 |
118 | # By default, name items with PascalCase
119 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
120 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
121 | dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style
122 |
123 | dotnet_naming_symbols.all_members.applicable_kinds = *
124 |
125 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
126 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
127 | dotnet_style_prefer_auto_properties = true:silent
128 | dotnet_style_prefer_collection_expression = true:suggestion
129 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
130 | tab_width = 4
131 | indent_size = 4
132 | end_of_line = crlf
133 |
134 | # CSharp code style settings:
135 | [*.cs]
136 | # Indentation preferences
137 | csharp_indent_block_contents = true
138 | csharp_indent_braces = false
139 | csharp_indent_case_contents = true
140 | csharp_indent_case_contents_when_block = true
141 | csharp_indent_switch_labels = true
142 | csharp_indent_labels = flush_left
143 |
144 | # Prefer "var" everywhere
145 | csharp_style_var_for_built_in_types = true:suggestion
146 | csharp_style_var_when_type_is_apparent = true:suggestion
147 | csharp_style_var_elsewhere = true:suggestion
148 |
149 | # Prefer method-like constructs to have a block body
150 | csharp_style_expression_bodied_methods = false:none
151 | csharp_style_expression_bodied_constructors = false:none
152 | csharp_style_expression_bodied_operators = false:none
153 |
154 | # Prefer property-like constructs to have an expression-body
155 | csharp_style_expression_bodied_properties = true:none
156 | csharp_style_expression_bodied_indexers = true:none
157 | csharp_style_expression_bodied_accessors = true:none
158 |
159 | # Suggest more modern language features when available
160 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
161 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
162 | csharp_style_inlined_variable_declaration = true:suggestion
163 | csharp_style_throw_expression = true:suggestion
164 | csharp_style_conditional_delegate_call = true:suggestion
165 |
166 | # Newline settings
167 | csharp_new_line_before_open_brace = all
168 | csharp_new_line_before_else = true
169 | csharp_new_line_before_catch = true
170 | csharp_new_line_before_finally = true
171 | csharp_new_line_before_members_in_object_initializers = true
172 | csharp_new_line_before_members_in_anonymous_types = true
173 | csharp_new_line_between_query_expression_clauses = true
174 |
175 | # Spacing
176 | csharp_space_after_cast = false
177 | csharp_space_after_colon_in_inheritance_clause = true
178 | csharp_space_after_keywords_in_control_flow_statements = true
179 | csharp_space_around_binary_operators = before_and_after
180 | csharp_space_before_colon_in_inheritance_clause = true
181 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
182 | csharp_space_between_method_call_name_and_opening_parenthesis = false
183 | csharp_space_between_method_call_parameter_list_parentheses = false
184 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
185 | csharp_space_between_method_declaration_parameter_list_parentheses = false
186 | csharp_space_between_parentheses = false
187 |
188 | # Blocks are allowed
189 | csharp_prefer_braces = true:silent
190 | csharp_preserve_single_line_blocks = true
191 | csharp_preserve_single_line_statements = true
192 | # Resharper
193 | csharp_wrap_before_comma = false
194 | csharp_place_attribute_on_same_line = false
195 | csharp_place_constructor_initializer_on_same_line = false
196 | csharp_place_expr_property_on_single_line = true
197 | csharp_place_expr_accessor_on_single_line = true
198 | csharp_wrap_before_first_type_parameter_constraint = true
199 | csharp_wrap_multiple_type_parameter_constraints_style = chop_always
200 | csharp_new_line_before_else = true
201 | csharp_new_line_before_while = true
202 | csharp_place_simple_embedded_block_on_same_line = false
203 | csharp_place_simple_initializer_on_single_line = true
204 | csharp_wrap_object_and_collection_initializer_style = chop_if_long
205 | csharp_wrap_array_initializer_style = chop_if_long
206 | csharp_wrap_after_dot_in_method_calls = false
207 | csharp_wrap_linq_expressions = chop_always
208 | csharp_wrap_before_linq_expression = true
209 | csharp_blank_lines_around_region = 1
210 | csharp_blank_lines_inside_region = 1
211 | csharp_remove_blank_lines_near_braces_in_declarations = true
212 | csharp_keep_blank_lines_in_declarations = 1
213 | csharp_remove_blank_lines_near_braces_in_declarations = true
214 | csharp_blank_lines_after_start_comment = 0
215 | csharp_blank_lines_after_using_list = 1
216 | csharp_blank_lines_inside_namespace = 0
217 | csharp_blank_lines_inside_type = 0
218 | csharp_blank_lines_around_field = 1
219 | csharp_blank_lines_around_single_line_field = 0
220 | csharp_blank_lines_around_property = 1
221 | csharp_blank_lines_around_single_line_property = 1
222 | csharp_blank_lines_around_auto_property = 1
223 | csharp_blank_lines_around_single_line_auto_property = 1
224 | csharp_blank_lines_around_invocable = 1
225 | csharp_blank_lines_around_single_line_invocable = 1
226 | csharp_remove_blank_lines_near_braces_in_code = true
227 | csharp_blank_lines_around_local_method = 1
228 | csharp_blank_lines_around_single_line_local_method = 1
229 | csharp_blank_lines_after_control_transfer_statements = 1
230 | csharp_blank_lines_after_multiline_statements = 1
231 | csharp_default_private_modifier = explicit
232 | csharp_braces_for_ifelse = required_for_multiline_statement
233 | csharp_braces_for_for = required
234 | csharp_braces_for_foreach = required
235 | csharp_braces_for_while = required
236 | csharp_braces_for_dowhile = required
237 | csharp_braces_for_using = required
238 | csharp_braces_for_lock = required
239 | csharp_braces_for_fixed = required
240 | csharp_method_or_operator_body = expression_body
241 | csharp_local_function_body = expression_body
242 | csharp_constructor_or_destructor_body = expression_body
243 | csharp_accessor_owner_body = expression_body
244 | csharp_force_attribute_style = separate
245 | csharp_brace_style = next_line
246 | csharp_anonymous_method_declaration_braces = next_line_shifted_2
247 | csharp_empty_block_style = multiline
248 | csharp_space_within_single_line_array_initializer_braces = true
249 | csharp_style_implicit_object_creation_when_type_is_apparent=false:suggestion
250 | csharp_style_namespace_declarations = file_scoped:warning
251 | csharp_using_directive_placement = outside_namespace:silent
252 | csharp_prefer_simple_using_statement = true:suggestion
253 | csharp_style_prefer_method_group_conversion = true:silent
254 | csharp_style_prefer_top_level_statements = true:silent
255 | csharp_style_prefer_primary_constructors = true:suggestion
256 | csharp_style_expression_bodied_lambdas = true:silent
257 | csharp_style_expression_bodied_local_functions = false:silent
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: .NET
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v3
21 | with:
22 | dotnet-version: 8.0.100-rc.1.23463.5
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from `dotnet new gitignore`
5 |
6 | # dotenv files
7 | .env
8 |
9 | # User-specific files
10 | *.rsuser
11 | *.suo
12 | *.user
13 | *.userosscache
14 | *.sln.docstates
15 |
16 | # User-specific files (MonoDevelop/Xamarin Studio)
17 | *.userprefs
18 |
19 | # Mono auto generated files
20 | mono_crash.*
21 |
22 | # Build results
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Ww][Ii][Nn]32/
30 | [Aa][Rr][Mm]/
31 | [Aa][Rr][Mm]64/
32 | bld/
33 | [Bb]in/
34 | [Oo]bj/
35 | [Ll]og/
36 | [Ll]ogs/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # Tye
69 | .tye/
70 |
71 | # ASP.NET Scaffolding
72 | ScaffoldingReadMe.txt
73 |
74 | # StyleCop
75 | StyleCopReport.xml
76 |
77 | # Files built by Visual Studio
78 | *_i.c
79 | *_p.c
80 | *_h.h
81 | *.ilk
82 | *.meta
83 | *.obj
84 | *.iobj
85 | *.pch
86 | *.pdb
87 | *.ipdb
88 | *.pgc
89 | *.pgd
90 | *.rsp
91 | *.sbr
92 | *.tlb
93 | *.tli
94 | *.tlh
95 | *.tmp
96 | *.tmp_proj
97 | *_wpftmp.csproj
98 | *.log
99 | *.tlog
100 | *.vspscc
101 | *.vssscc
102 | .builds
103 | *.pidb
104 | *.svclog
105 | *.scc
106 |
107 | # Chutzpah Test files
108 | _Chutzpah*
109 |
110 | # Visual C++ cache files
111 | ipch/
112 | *.aps
113 | *.ncb
114 | *.opendb
115 | *.opensdf
116 | *.sdf
117 | *.cachefile
118 | *.VC.db
119 | *.VC.VC.opendb
120 |
121 | # Visual Studio profiler
122 | *.psess
123 | *.vsp
124 | *.vspx
125 | *.sap
126 |
127 | # Visual Studio Trace Files
128 | *.e2e
129 |
130 | # TFS 2012 Local Workspace
131 | $tf/
132 |
133 | # Guidance Automation Toolkit
134 | *.gpState
135 |
136 | # ReSharper is a .NET coding add-in
137 | _ReSharper*/
138 | *.[Rr]e[Ss]harper
139 | *.DotSettings.user
140 |
141 | # TeamCity is a build add-in
142 | _TeamCity*
143 |
144 | # DotCover is a Code Coverage Tool
145 | *.dotCover
146 |
147 | # AxoCover is a Code Coverage Tool
148 | .axoCover/*
149 | !.axoCover/settings.json
150 |
151 | # Coverlet is a free, cross platform Code Coverage Tool
152 | coverage*.json
153 | coverage*.xml
154 | coverage*.info
155 |
156 | # Visual Studio code coverage results
157 | *.coverage
158 | *.coveragexml
159 |
160 | # NCrunch
161 | _NCrunch_*
162 | .*crunch*.local.xml
163 | nCrunchTemp_*
164 |
165 | # MightyMoose
166 | *.mm.*
167 | AutoTest.Net/
168 |
169 | # Web workbench (sass)
170 | .sass-cache/
171 |
172 | # Installshield output folder
173 | [Ee]xpress/
174 |
175 | # DocProject is a documentation generator add-in
176 | DocProject/buildhelp/
177 | DocProject/Help/*.HxT
178 | DocProject/Help/*.HxC
179 | DocProject/Help/*.hhc
180 | DocProject/Help/*.hhk
181 | DocProject/Help/*.hhp
182 | DocProject/Help/Html2
183 | DocProject/Help/html
184 |
185 | # Click-Once directory
186 | publish/
187 |
188 | # Publish Web Output
189 | *.[Pp]ublish.xml
190 | *.azurePubxml
191 | # Note: Comment the next line if you want to checkin your web deploy settings,
192 | # but database connection strings (with potential passwords) will be unencrypted
193 | *.pubxml
194 | *.publishproj
195 |
196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
197 | # checkin your Azure Web App publish settings, but sensitive information contained
198 | # in these scripts will be unencrypted
199 | PublishScripts/
200 |
201 | # NuGet Packages
202 | *.nupkg
203 | # NuGet Symbol Packages
204 | *.snupkg
205 | # The packages folder can be ignored because of Package Restore
206 | **/[Pp]ackages/*
207 | # except build/, which is used as an MSBuild target.
208 | !**/[Pp]ackages/build/
209 | # Uncomment if necessary however generally it will be regenerated when needed
210 | #!**/[Pp]ackages/repositories.config
211 | # NuGet v3's project.json files produces more ignorable files
212 | *.nuget.props
213 | *.nuget.targets
214 |
215 | # Microsoft Azure Build Output
216 | csx/
217 | *.build.csdef
218 |
219 | # Microsoft Azure Emulator
220 | ecf/
221 | rcf/
222 |
223 | # Windows Store app package directories and files
224 | AppPackages/
225 | BundleArtifacts/
226 | Package.StoreAssociation.xml
227 | _pkginfo.txt
228 | *.appx
229 | *.appxbundle
230 | *.appxupload
231 |
232 | # Visual Studio cache files
233 | # files ending in .cache can be ignored
234 | *.[Cc]ache
235 | # but keep track of directories ending in .cache
236 | !?*.[Cc]ache/
237 |
238 | # Others
239 | ClientBin/
240 | ~$*
241 | *~
242 | *.dbmdl
243 | *.dbproj.schemaview
244 | *.jfm
245 | *.pfx
246 | *.publishsettings
247 | orleans.codegen.cs
248 |
249 | # Including strong name files can present a security risk
250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
251 | #*.snk
252 |
253 | # Since there are multiple workflows, uncomment next line to ignore bower_components
254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
255 | #bower_components/
256 |
257 | # RIA/Silverlight projects
258 | Generated_Code/
259 |
260 | # Backup & report files from converting an old project file
261 | # to a newer Visual Studio version. Backup files are not needed,
262 | # because we have git ;-)
263 | _UpgradeReport_Files/
264 | Backup*/
265 | UpgradeLog*.XML
266 | UpgradeLog*.htm
267 | ServiceFabricBackup/
268 | *.rptproj.bak
269 |
270 | # SQL Server files
271 | *.mdf
272 | *.ldf
273 | *.ndf
274 |
275 | # Business Intelligence projects
276 | *.rdl.data
277 | *.bim.layout
278 | *.bim_*.settings
279 | *.rptproj.rsuser
280 | *- [Bb]ackup.rdl
281 | *- [Bb]ackup ([0-9]).rdl
282 | *- [Bb]ackup ([0-9][0-9]).rdl
283 |
284 | # Microsoft Fakes
285 | FakesAssemblies/
286 |
287 | # GhostDoc plugin setting file
288 | *.GhostDoc.xml
289 |
290 | # Node.js Tools for Visual Studio
291 | .ntvs_analysis.dat
292 | node_modules/
293 |
294 | # Visual Studio 6 build log
295 | *.plg
296 |
297 | # Visual Studio 6 workspace options file
298 | *.opt
299 |
300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
301 | *.vbw
302 |
303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
304 | *.vbp
305 |
306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
307 | *.dsw
308 | *.dsp
309 |
310 | # Visual Studio 6 technical files
311 | *.ncb
312 | *.aps
313 |
314 | # Visual Studio LightSwitch build output
315 | **/*.HTMLClient/GeneratedArtifacts
316 | **/*.DesktopClient/GeneratedArtifacts
317 | **/*.DesktopClient/ModelManifest.xml
318 | **/*.Server/GeneratedArtifacts
319 | **/*.Server/ModelManifest.xml
320 | _Pvt_Extensions
321 |
322 | # Paket dependency manager
323 | .paket/paket.exe
324 | paket-files/
325 |
326 | # FAKE - F# Make
327 | .fake/
328 |
329 | # CodeRush personal settings
330 | .cr/personal
331 |
332 | # Python Tools for Visual Studio (PTVS)
333 | __pycache__/
334 | *.pyc
335 |
336 | # Cake - Uncomment if you are using it
337 | # tools/**
338 | # !tools/packages.config
339 |
340 | # Tabs Studio
341 | *.tss
342 |
343 | # Telerik's JustMock configuration file
344 | *.jmconfig
345 |
346 | # BizTalk build output
347 | *.btp.cs
348 | *.btm.cs
349 | *.odx.cs
350 | *.xsd.cs
351 |
352 | # OpenCover UI analysis results
353 | OpenCover/
354 |
355 | # Azure Stream Analytics local run output
356 | ASALocalRun/
357 |
358 | # MSBuild Binary and Structured Log
359 | *.binlog
360 |
361 | # NVidia Nsight GPU debugger configuration file
362 | *.nvuser
363 |
364 | # MFractors (Xamarin productivity tool) working folder
365 | .mfractor/
366 |
367 | # Local History for Visual Studio
368 | .localhistory/
369 |
370 | # Visual Studio History (VSHistory) files
371 | .vshistory/
372 |
373 | # BeatPulse healthcheck temp database
374 | healthchecksdb
375 |
376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
377 | MigrationBackup/
378 |
379 | # Ionide (cross platform F# VS Code tools) working folder
380 | .ionide/
381 |
382 | # Fody - auto-generated XML schema
383 | FodyWeavers.xsd
384 |
385 | # VS Code files for those working on multiple tools
386 | .vscode/*
387 | !.vscode/settings.json
388 | !.vscode/tasks.json
389 | !.vscode/launch.json
390 | !.vscode/extensions.json
391 | *.code-workspace
392 |
393 | # Local History for Visual Studio Code
394 | .history/
395 |
396 | # Windows Installer files from build outputs
397 | *.cab
398 | *.msi
399 | *.msix
400 | *.msm
401 | *.msp
402 |
403 | # JetBrains Rider
404 | *.sln.iml
405 | .idea
406 |
407 | ##
408 | ## Visual studio for Mac
409 | ##
410 |
411 |
412 | # globs
413 | Makefile.in
414 | *.userprefs
415 | *.usertasks
416 | config.make
417 | config.status
418 | aclocal.m4
419 | install-sh
420 | autom4te.cache/
421 | *.tar.gz
422 | tarballs/
423 | test-results/
424 |
425 | # Mac bundle stuff
426 | *.dmg
427 | *.app
428 |
429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
430 | # General
431 | .DS_Store
432 | .AppleDouble
433 | .LSOverride
434 |
435 | # Icon must end with two \r
436 | Icon
437 |
438 |
439 | # Thumbnails
440 | ._*
441 |
442 | # Files that might appear in the root of a volume
443 | .DocumentRevisions-V100
444 | .fseventsd
445 | .Spotlight-V100
446 | .TemporaryItems
447 | .Trashes
448 | .VolumeIcon.icns
449 | .com.apple.timemachine.donotpresent
450 |
451 | # Directories potentially created on remote AFP share
452 | .AppleDB
453 | .AppleDesktop
454 | Network Trash Folder
455 | Temporary Items
456 | .apdisk
457 |
458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
459 | # Windows thumbnail cache files
460 | Thumbs.db
461 | ehthumbs.db
462 | ehthumbs_vista.db
463 |
464 | # Dump file
465 | *.stackdump
466 |
467 | # Folder config file
468 | [Dd]esktop.ini
469 |
470 | # Recycle Bin used on file shares
471 | $RECYCLE.BIN/
472 |
473 | # Windows Installer files
474 | *.cab
475 | *.msi
476 | *.msix
477 | *.msm
478 | *.msp
479 |
480 | # Windows shortcuts
481 | *.lnk
482 |
483 | # Vim temporary swap files
484 | *.swp
485 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | false
5 | Nullable
6 | enable
7 |
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Ilchert
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.nuget.org/packages/SystemTextJson.FluentApi/ "Download SystemTextJson.FluentApi from NuGet.org")
2 |
3 | Normally you do not need to use this package and just copy paste required functionality like NRT or polymorphism support.
4 | # SystemTextJson.FluentApi
5 | SystemTextJson.FluentApi is a fluent configuration library for System.Text.Json that allows developers to configure serialization uses strongly typed fluent interface and lambda expression.
6 |
7 | # Documentation
8 | All api usually repeats attributes from [System.Text.Json.Serialization](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization) and set corresponding property in [JsonPropertyInfo](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsonpropertyinfo?view=net-7.0) or [JsonTypeInfo](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsontypeinfo?view=net-7.0). Configuration based on [IJsonTypeInfoResolver](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.ijsontypeinforesolver) so developers can configure reflection based [TypeInfoResolver](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.defaultjsontypeinforesolver) and source generator [JsonSerializerContext](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonserializercontext).
9 |
10 | # Quick start
11 | To use FluentApi need to configure [JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions) instance via `JsonModelBuilder` and pass it to serializer.
12 |
13 | ```C#
14 | var options = new JsonSerializerOptions() { WriteIndented = true };
15 | options.ConfigureDefaultTypeResolver(p =>
16 | p.Entity()
17 | .Property(p => p.LastName).HasName("surname")
18 | .Property(p => p.FirstName).IsIgnored()
19 | .VirtualProperty("FullName", p => $"{p.FirstName} {p.LastName}")
20 | .Property(p => p.Age).HasHumberHandling(JsonNumberHandling.WriteAsString));
21 |
22 | var person = new Person() { FirstName = "First name", LastName = "Last name", Age = 12 };
23 | var json = JsonSerializer.Serialize(person, options);
24 |
25 | Console.WriteLine(json);
26 | ```
27 |
28 | This example produce this JSON
29 | ```Json
30 | {
31 | "surname": "Last name",
32 | "Age": "12",
33 | "FullName": "First name Last name"
34 | }
35 | ```
36 |
37 | # Polymorphism serialization
38 | STJ has build in support polymorphic serialization, but user have to annotate base class with [JsonDerivedTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonderivedtypeattribute) with all derived types. In fluent API you can configure each derived type manually or find all derived types in runtime.
39 |
40 | ```C#
41 | builder.Entity()
42 | .HasDerivedType(nameof(Derived1))
43 | .HasDerivedType(nameof(Derived2))
44 | .HasDerivedType(nameof(Root))
45 | // or
46 | builder.Entity().HasDerivedTypesFromAssembly(Assembly.GetExecutingAssembly(), t => t.Name)
47 |
48 | var testObject = new Root[]
49 | {
50 | new Derived1() { Derived1Property = "derived" },
51 | new Derived2() { Derived2Property = "derived2" },
52 | new Root(){ RootProperty = "root"}
53 | };
54 |
55 |
56 | public class Root
57 | {
58 | public string? RootProperty { get; set; }
59 | }
60 |
61 | public class Derived1 : Root
62 | {
63 | public string? Derived1Property { get; set; }
64 | }
65 |
66 | public class Derived2 : Root
67 | {
68 | public string? Derived2Property { get; set; }
69 | }
70 | ```
71 |
72 | Serialization of `testObject` collection will produce:
73 |
74 | ```JSON
75 | [
76 | {
77 | "$type":"Derived1",
78 | "Derived1Property":"derived",
79 | "RootProperty":null
80 | },
81 | {
82 | "$type":"Derived2",
83 | "Derived2Property":"derived2",
84 | "RootProperty":null
85 | },
86 | {
87 | "$type":"Root",
88 | "RootProperty":"root"
89 | }
90 | ]
91 | ```
92 |
93 | With `$type` discriminator serializer are able to deserialize this collection. Another approach to serialization is use actual type from object instance, instead of property type. To achieve this behavior serializer can threat specific property as `object` using `PropertyBuilder.SerializeAsObject`.
94 |
95 | ```C#
96 | builder.Entity().Property(p => p.Data).SerializeAsObject();
97 |
98 | var testObject = new AsObjectTestClass { Data = new Derived() { Property = "Prop" } };
99 |
100 | public class AsObjectTestClass
101 | {
102 | public Root? Data { get; set; }
103 | }
104 |
105 | public class Root { }
106 |
107 | public class Derived : Root
108 | {
109 | public string? Property { get; set; }
110 | }
111 |
112 | ```
113 |
114 | Serialization of `testObject` will produce:
115 | ```JSON
116 | {
117 | "Data":{
118 | "Property":"Prop"
119 | }
120 | }
121 | ```
122 |
123 | But in this case only serialization is available because JSON does not contain type discriminator and `JsonException` will be thrown on deserialization.
124 |
125 | # Nullable reference type support
126 | STJ has build in support of [`required`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required) properties, but it just check, that value exists in JSON on deserialization and does not prevent setting `null` to none nullable properties. Fluent Api can configure `JsonSerializerOptions` to respect NRT annotations on fields and properties. Internally it uses [JsonPropertyInfo.Set](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsonpropertyinfo.set) property and reduces deserialization performance.
127 | STJ will implement full NRT support in [.net9](https://github.com/dotnet/runtime/issues/100144)
128 |
129 | ```C#
130 | builder.RespectNullableReferenceType();
131 |
132 | JsonSerializer.Deserialize("""{"Property": null}""", _options); // this throws JsonException
133 | JsonAsserts.AssertObject(new TestClass(), "{}", _options); // BUT this is not because Property is not requred.
134 |
135 | public class TestClass
136 | {
137 | public string Property { get; set; }
138 | }
139 | ```
140 |
141 | # Virtual properties
142 |
143 | Fluent Api can define virtual properties, that does not match to any real property in object.
144 |
145 | ```C#
146 | builder.Entity()
147 | .Property(p => p.LastName).IsIgnored()
148 | .Property(p => p.FirstName).IsIgnored()
149 | .VirtualProperty("FullName", p => $"{p.FirstName} {p.LastName}")
150 |
151 | var testObject = new Person() { FirstName = "First name", LastName = "Last name" };
152 |
153 | class Person
154 | {
155 | public string? FirstName { get; set; }
156 |
157 | public string? LastName { get; set; }
158 | }
159 |
160 | ```
161 |
162 | Serialization of `testObject` will produce:
163 |
164 | ```JSON
165 | {
166 | "FullName": "First name Last name"
167 | }
168 | ```
169 |
170 | # Change tracking
171 |
172 | Fluent Api can track changes during serialization and deserialization. If some entity implement `IHaveChnagedProperties` interface with not null `ChangedProperties` property, it will be used to track changes. To populate property/field names that set deserialization use `TrackChangedProperties()` method. To serialize properties only from `ChangedProperties` use `SerializeOnlyChangedProperties()`. This method will override `JsonIgnoreCondition`.
173 |
174 | ```C#
175 | builder.TrackChangedProperties().SerializeOnlyChangedProperties();
176 |
177 | var testObject = new TrackTestClass()
178 | {
179 | StringProperty = "str",
180 | IntProperty = 1,
181 | ChangedProperties = { nameof(TrackTestClass.IntProperty) }
182 | };
183 |
184 | public class TrackTestClass : IHaveChangedProperties
185 | {
186 | public string? StringProperty { get; set; }
187 | public int IntProperty { get; set; }
188 | public ISet ChangedProperties { get; } = new HashSet();
189 | }
190 |
191 | ```
192 |
193 | Serialization of `testObject` will produce:
194 |
195 | ```JSON
196 | {
197 | "IntProperty": 1
198 | }
199 | ```
200 | And deserialization of this JSON will populate `"IntProperty"` value to `ChangedProperties`.
201 |
202 | # ValueTuple serialization
203 |
204 | Fluent Api has `ValueTupleJsonConverter` to serialize and deserialize `ValueTuple` as array.
205 |
206 | ```C#
207 | var options = new JsonSerializerOptions() { Converters = { new ValueTupleJsonConverter() } };
208 | JsonSerializer.Serialize((1,"str"),options);
209 | ```
210 |
211 | This code output:
212 |
213 | ```JSON
214 | [1,"str"]
215 | ```
216 |
217 | # Inline arrays support
218 |
219 | Fluent Api has `InlineArrayJsonConverter` for .NET 8 and above to serialize and deserialize [`InlineArray`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays) structs as arrays.
220 |
221 | ```C#
222 | var array = new InlineArray();
223 | array[0] = null;
224 | array[1] = 1;
225 | array[2] = -1;
226 | var options = new JsonSerializerOptions() { Converters = { new InlineArrayJsonConverter() } };
227 | JsonSerializer.Serialize(array,options);
228 |
229 | [InlineArray(3)]
230 | private struct InlineArray
231 | {
232 | public int? Value;
233 | }
234 | ```
235 |
236 | Output: `"[null,1,-1]"`
237 |
238 | # Enum with JsonPropertyName attributes
239 |
240 | Default [JsonStringEnumConverter](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter) does not respect `JsonPropertyNameAttribute`, to fix it use CustomizableJsonStringEnumConverter instead.
241 |
242 | ```C#
243 | // set globally
244 | var options = new JsonSerializerOptions() { Converters = { new CustomizableJsonStringEnumConverter(JsonNamingPolicy.CamelCase) } };
245 | // or for scpecific enum
246 | var options = new JsonSerializerOptions().ConfigureEnumValues(
247 | new Dictionary { { A.First, "f" } },
248 | namingPolicy: JsonNamingPolicy.CamelCase);
249 | JsonSerializer.Serialize([A.First, null, A.Third, (A)8], _options);
250 |
251 | enum A
252 | {
253 | [JsonPropertyName("f")]
254 | First,
255 | Second,
256 | Third
257 | }
258 | ```
259 |
260 | Output:
261 | ```JS
262 | ["f",null,"third",8]
263 | ```
264 |
--------------------------------------------------------------------------------
/SystemTextJson.FluentApi.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.8.34112.27
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.FluentApi", "src\SystemTextJson.FluentApi\SystemTextJson.FluentApi.csproj", "{BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.FluentApi.Tests", "tests\SystemTextJson.FluentApi.Tests\SystemTextJson.FluentApi.Tests.csproj", "{1AD3BC43-4E56-418E-BA9F-11DDD0028C74}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.FluentApi.QuickStart", "samples\SystemTextJson.FluentApi.QuickStart\SystemTextJson.FluentApi.QuickStart.csproj", "{220BD50B-2A8F-4000-B984-3AB75FB24D93}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{D66C1040-8D07-436A-AD30-E1888B556441}"
13 | ProjectSection(SolutionItems) = preProject
14 | .editorconfig = .editorconfig
15 | Directory.Build.props = Directory.Build.props
16 | LICENSE = LICENSE
17 | README.md = README.md
18 | EndProjectSection
19 | EndProject
20 | Global
21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
22 | Debug|Any CPU = Debug|Any CPU
23 | Release|Any CPU = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Release|Any CPU.Build.0 = Release|Any CPU
38 | EndGlobalSection
39 | GlobalSection(SolutionProperties) = preSolution
40 | HideSolutionNode = FALSE
41 | EndGlobalSection
42 | GlobalSection(ExtensibilityGlobals) = postSolution
43 | SolutionGuid = {03C213A2-7B38-4322-AA09-5C73DE365869}
44 | EndGlobalSection
45 | EndGlobal
46 |
--------------------------------------------------------------------------------
/samples/SystemTextJson.FluentApi.QuickStart/Program.cs:
--------------------------------------------------------------------------------
1 | // See https://aka.ms/new-console-template for more information
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 | using SystemTextJson.FluentApi;
5 |
6 | var options = new JsonSerializerOptions() { WriteIndented = true, Converters = { new ValueTupleJsonConverter() } };
7 | options.ConfigureDefaultTypeResolver(p =>
8 | p.Entity()
9 | .Property(p => p.LastName).HasName("surname")
10 | .Property(p => p.FirstName).IsIgnored()
11 | .VirtualProperty("FullName", p => $"{p?.FirstName} {p?.LastName}")
12 | .Property(p => p.Age).HasHumberHandling(JsonNumberHandling.WriteAsString));
13 |
14 | var person = new Person() { FirstName = "First name", LastName = "Last name", Age = 12 };
15 | var json = JsonSerializer.Serialize(person, options);
16 |
17 | Console.WriteLine(json);
18 |
19 | var tupleJson = JsonSerializer.Serialize((1,"str"),options);
20 | Console.WriteLine(tupleJson);
21 |
22 | class Person
23 | {
24 | public string? FirstName { get; set; }
25 |
26 | public string? LastName { get; set; }
27 |
28 | public int Age { get; set; }
29 | }
30 |
--------------------------------------------------------------------------------
/samples/SystemTextJson.FluentApi.QuickStart/SystemTextJson.FluentApi.QuickStart.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/CustomEnumConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace SystemTextJson.FluentApi;
6 |
7 | internal class CustomEnumConverter : JsonConverter
8 | where T : struct, Enum
9 | {
10 | private readonly JsonConverter _inner;
11 | private readonly Dictionary _valuesCache;
12 | private readonly Dictionary _namesCache;
13 |
14 | public CustomEnumConverter(JsonConverter inner, JsonSerializerOptions options)
15 | {
16 | _inner = inner;
17 | _valuesCache = [];
18 | _namesCache = [];
19 |
20 | foreach (var field in typeof(T).GetFields())
21 | {
22 | if (!field.IsLiteral)
23 | continue;
24 |
25 | var attribute = field.GetCustomAttribute();
26 | if (attribute is null)
27 | continue;
28 |
29 | _valuesCache[attribute.Name] = (T)field.GetRawConstantValue()!;
30 | _namesCache[(T)field.GetRawConstantValue()!] = JsonEncodedText.Encode(attribute.Name, options.Encoder);
31 | }
32 |
33 | _inner = inner;
34 | }
35 |
36 | public CustomEnumConverter(JsonConverter inner, JsonSerializerOptions options, IReadOnlyDictionary mapping)
37 | {
38 | _inner = inner;
39 | _valuesCache = [];
40 | _namesCache = [];
41 |
42 | foreach (var kv in mapping)
43 | {
44 | _valuesCache[kv.Value] = kv.Key;
45 | _namesCache[kv.Key] = JsonEncodedText.Encode(kv.Value, options.Encoder);
46 | }
47 |
48 | _inner = inner;
49 | }
50 |
51 | public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
52 | {
53 | if (reader.TokenType == JsonTokenType.String)
54 | {
55 | var valueStr = reader.GetString();
56 | if (valueStr is null)
57 | return default;
58 |
59 | if (_valuesCache.TryGetValue(valueStr, out var value))
60 | return value;
61 | }
62 |
63 | return _inner.Read(ref reader, typeToConvert, options);
64 | }
65 |
66 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
67 | {
68 | if (_namesCache.TryGetValue(value, out var valueStr))
69 | writer.WriteStringValue(valueStr);
70 | else
71 | _inner.Write(writer, value, options);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/CustomizableJsonStringEnumConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace SystemTextJson.FluentApi;
6 | public class CustomizableJsonStringEnumConverter : JsonConverterFactory
7 | {
8 | private readonly JsonStringEnumConverter _defaultConverter;
9 | public CustomizableJsonStringEnumConverter() =>
10 | _defaultConverter = new JsonStringEnumConverter();
11 |
12 | public CustomizableJsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true) =>
13 | _defaultConverter = new JsonStringEnumConverter(namingPolicy, allowIntegerValues);
14 |
15 | public override bool CanConvert(Type typeToConvert) =>
16 | _defaultConverter.CanConvert(typeToConvert);
17 |
18 | public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
19 | {
20 | var defaultConverter = _defaultConverter.CreateConverter(typeToConvert, options);
21 |
22 | foreach (var field in typeToConvert.GetFields())
23 | {
24 | if (!field.IsLiteral)
25 | continue;
26 |
27 | var attribute = field.GetCustomAttribute();
28 | if (attribute is null)
29 | continue;
30 |
31 | var type = typeof(CustomEnumConverter<>).MakeGenericType(typeToConvert);
32 | return (JsonConverter)Activator.CreateInstance(type, defaultConverter, options)!;
33 | }
34 |
35 | return defaultConverter;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/EntityTypeBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization.Metadata;
2 |
3 | namespace SystemTextJson.FluentApi;
4 |
5 | public class EntityTypeBuilder(JsonModelBuilder modelBuilder) : IEntityTypeBuilder
6 | {
7 | public Type EntityType => typeof(TEntity);
8 | public IList> JsonTypeInfoActions { get; } = [];
9 | public IList PropertyBuilders { get; } = [];
10 |
11 | public JsonModelBuilder ModelBuilder { get; } = modelBuilder;
12 |
13 | public EntityTypeBuilder ConfigureTyped(Action> configureAction)
14 | {
15 | JsonTypeInfoActions.Add(p => configureAction((JsonTypeInfo)p));
16 | return this;
17 | }
18 |
19 | public EntityTypeBuilder HasDerivedType() where T : TEntity =>
20 | this.HasDerivedType(typeof(T));
21 |
22 | public EntityTypeBuilder HasDerivedType(string typeDiscriminator) where T : TEntity =>
23 | this.HasDerivedType(typeof(T), typeDiscriminator);
24 |
25 | public EntityTypeBuilder HasDerivedType(int typeDiscriminator) where T : TEntity =>
26 | this.HasDerivedType(typeof(T), typeDiscriminator);
27 |
28 | Action IEntityTypeBuilder.Build()
29 | {
30 | var memberProperties = PropertyBuilders.OfType()
31 | .GroupBy(p => p.MemberInfo, p => p.Build())
32 | .Select(p => (p.Key, Value: (Action)Delegate.Combine(p.ToArray())!))
33 | .ToDictionary(p => p.Key, p => p.Value);
34 |
35 | var namedProperties = PropertyBuilders.Where(p => p is not IMemberPropertyBuilder).
36 | GroupBy(p => p.Name, p => p.Build())
37 | .Select(p => (p.Key, Value: (Action)Delegate.Combine(p.ToArray())!))
38 | .ToDictionary(p => p.Key, p => p.Value);
39 |
40 | var typeConfigurations = JsonTypeInfoActions.ToArray();
41 | return p =>
42 | {
43 | foreach (var tc in typeConfigurations)
44 | tc(p);
45 |
46 | foreach (var prop in p.Properties)
47 | {
48 | var mi = prop.GetMemberInfo();
49 | Action? propertyConfig = null;
50 |
51 | if (mi is null)
52 | namedProperties.TryGetValue(prop.Name, out propertyConfig);
53 | else
54 | memberProperties.TryGetValue(mi, out propertyConfig);
55 |
56 | propertyConfig?.Invoke(prop);
57 | }
58 | };
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/EntityTypeBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using System.Reflection;
3 | using System.Text.Json.Serialization;
4 | using System.Text.Json.Serialization.Metadata;
5 |
6 | namespace SystemTextJson.FluentApi;
7 |
8 | public static class EntityTypeBuilderExtensions
9 | {
10 | public static T Configure(this T builder, Action configureAction) where T : IEntityTypeBuilder
11 | {
12 | builder.JsonTypeInfoActions.Add(configureAction);
13 | return builder;
14 | }
15 |
16 | public static T IsUnmappedMemberDisallowed(this T builder) where T : IEntityTypeBuilder =>
17 | builder.Configure(p => p.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow);
18 |
19 | public static T HasTypeDiscriminator(this T builder, string typeDiscriminator) where T : IEntityTypeBuilder =>
20 | builder.Configure(p =>
21 | {
22 | p.PolymorphismOptions ??= new JsonPolymorphismOptions();
23 | p.PolymorphismOptions.TypeDiscriminatorPropertyName = typeDiscriminator;
24 | });
25 |
26 | public static T HasDerivedType(this T builder, Type derivedType) where T : IEntityTypeBuilder =>
27 | builder.HasDerivedType(new JsonDerivedType(derivedType));
28 |
29 | public static T HasDerivedType(this T builder, Type derivedType, string typeDiscriminator) where T : IEntityTypeBuilder =>
30 | builder.HasDerivedType(new JsonDerivedType(derivedType, typeDiscriminator));
31 |
32 | public static T HasDerivedType(this T builder, Type derivedType, int typeDiscriminator) where T : IEntityTypeBuilder =>
33 | builder.HasDerivedType(new JsonDerivedType(derivedType, typeDiscriminator));
34 |
35 | public static T HasDerivedType(this T builder, JsonDerivedType derivedType) where T : IEntityTypeBuilder
36 | {
37 | return builder.Configure(p =>
38 | {
39 | p.PolymorphismOptions ??= new JsonPolymorphismOptions();
40 | p.PolymorphismOptions.DerivedTypes.Add(derivedType);
41 | });
42 | }
43 |
44 | public static T HasDerivedType(this T builder, params JsonDerivedType[] derivedTypes) where T : IEntityTypeBuilder
45 | {
46 | return builder.Configure(p =>
47 | {
48 | p.PolymorphismOptions ??= new JsonPolymorphismOptions();
49 | foreach (var derivedType in derivedTypes)
50 | p.PolymorphismOptions.DerivedTypes.Add(derivedType);
51 | });
52 | }
53 |
54 | public static T HasDerivedTypesFromAssembly(this T builder, Assembly assembly, Func? discriminatorFormatter = null) where T : IEntityTypeBuilder
55 | {
56 | Type[] types;
57 | try
58 | {
59 | types = assembly.GetTypes();
60 | }
61 | catch (ReflectionTypeLoadException ex)
62 | {
63 | types = ex.Types.Where(p => p != null).ToArray()!;
64 | }
65 |
66 | types = types.Where(p => p != null && builder.EntityType.IsAssignableFrom(p)).ToArray();
67 | if (types.Length == 0)
68 | return builder;
69 |
70 | var jsonTypes = discriminatorFormatter is null ?
71 | types.Select(p => new JsonDerivedType(p)) :
72 | types.Select(p => new JsonDerivedType(p, discriminatorFormatter(p)));
73 |
74 | builder.HasDerivedType(jsonTypes.ToArray());
75 |
76 | return builder;
77 | }
78 |
79 | public static VirtualPropertyBuilder VirtualProperty(this IEntityTypeBuilder builder, string name, Func compute)
80 | {
81 | builder.Configure(p =>
82 | {
83 | if (p.Properties.Any(p => p.Name == name))
84 | return;
85 | var propInfo = p.CreateJsonPropertyInfo(typeof(TProperty), name);
86 | propInfo.Get = (o) => compute((TEntity)o);
87 | p.Properties.Add(propInfo);
88 | });
89 | var newBuilder = new VirtualPropertyBuilder(name, builder);
90 | builder.PropertyBuilders.Add(newBuilder);
91 | return newBuilder;
92 | }
93 |
94 |
95 | public static MemberPropertyBuilder Property(this IEntityTypeBuilder builder, Expression> propertyExpression)
96 | {
97 | var mi = GetMemberInfo(propertyExpression);
98 | var newBuilder = new MemberPropertyBuilder(mi, builder);
99 | builder.PropertyBuilders.Add(newBuilder);
100 | return newBuilder;
101 | }
102 |
103 | private static MemberInfo GetMemberInfo(Expression> propertyExpression)
104 | {
105 | if (propertyExpression.Body is not MemberExpression member)
106 | throw new ArgumentException($"Expression '{propertyExpression}' refers to a method, not a property.");
107 |
108 | if (member.Member is not (PropertyInfo or FieldInfo))
109 | throw new ArgumentException($"Expression '{propertyExpression}' refers to a field, not a property.");
110 |
111 | return member.Member;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/IEntityTypeBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.Json.Serialization.Metadata;
3 |
4 | namespace SystemTextJson.FluentApi;
5 |
6 | public interface IEntityTypeBuilder
7 | {
8 | Type EntityType { get; }
9 | internal IList> JsonTypeInfoActions { get; }
10 |
11 | internal IList PropertyBuilders { get; }
12 |
13 | JsonModelBuilder ModelBuilder { get; }
14 |
15 | internal Action Build();
16 | }
17 |
18 | public interface IEntityTypeBuilder : IEntityTypeBuilder
19 | {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/IHaveChangedProperties.cs:
--------------------------------------------------------------------------------
1 | namespace SystemTextJson.FluentApi;
2 | public interface IHaveChangedProperties
3 | {
4 | ISet? ChangedProperties { get; }
5 | }
6 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/IMemberPropertyBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace SystemTextJson.FluentApi;
4 |
5 | public interface IMemberPropertyBuilder : IPropertyBuilder
6 | {
7 | public MemberInfo MemberInfo { get; }
8 | }
9 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/IPropertyBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization.Metadata;
2 |
3 | namespace SystemTextJson.FluentApi;
4 |
5 | public interface IPropertyBuilder
6 | {
7 | string Name { get; }
8 |
9 | internal IList> JsonPropertyInfoActions { get; }
10 |
11 | internal IEntityTypeBuilder EntityTypeBuilder { get; }
12 |
13 | internal Action Build();
14 | }
15 | public interface IPropertyBuilder : IPropertyBuilder
16 | {
17 | new IEntityTypeBuilder EntityTypeBuilder { get; }
18 | }
19 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/InlineArrayJsonConverter.cs:
--------------------------------------------------------------------------------
1 |
2 | using System.Reflection;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 | using System.Runtime.CompilerServices;
6 | using System.Runtime.InteropServices;
7 | using static SystemTextJson.FluentApi.SerializationHelpers;
8 | namespace SystemTextJson.FluentApi;
9 | #if NET8_0_OR_GREATER
10 |
11 | public class InlineArrayJsonConverter : JsonConverterFactory
12 | {
13 | public override bool CanConvert(Type typeToConvert) =>
14 | typeToConvert.GetCustomAttribute() != null;
15 |
16 | public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
17 | {
18 | var attribute = typeToConvert.GetCustomAttribute();
19 | if (attribute is null)
20 | return null;
21 |
22 | var length = attribute.Length;
23 | var itemType = typeToConvert.GetFields()[0].FieldType; // inline array can have only one field
24 |
25 | var converterType = typeof(ConcreteInlineArrayJsonConverter<,>).MakeGenericType(typeToConvert, itemType);
26 | return (JsonConverter)Activator.CreateInstance(converterType, length, options)!;
27 | }
28 |
29 | private class ConcreteInlineArrayJsonConverter(int length, JsonSerializerOptions options) : JsonConverter
30 | where TStruct : struct
31 | {
32 | private readonly JsonConverter _itemConverter = (JsonConverter)options.GetConverter(typeof(TItem));
33 | public override TStruct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
34 | {
35 | if (reader.TokenType != JsonTokenType.StartArray)
36 | throw new JsonException("Start token must be '['.");
37 |
38 | reader.Read();
39 |
40 | var result = default(TStruct);
41 | var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref result), length);
42 | for (var i = 0; i < span.Length; i++)
43 | span[i] = ReadValue(_itemConverter, ref reader, options);
44 |
45 | if (reader.TokenType != JsonTokenType.EndArray)
46 | throw new JsonException("Expected end token ']'.");
47 |
48 | return result;
49 | }
50 |
51 | public override void Write(Utf8JsonWriter writer, TStruct value, JsonSerializerOptions options)
52 | {
53 | writer.WriteStartArray();
54 |
55 | var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref value), length);
56 | for (var i = 0; i < span.Length; i++)
57 | WriteValue(_itemConverter, writer, span[i], options);
58 |
59 | writer.WriteEndArray();
60 | }
61 | }
62 |
63 | }
64 | #endif
65 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/JsonModelBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization.Metadata;
4 |
5 | namespace SystemTextJson.FluentApi;
6 |
7 | public sealed class JsonModelBuilder
8 | {
9 | private readonly Dictionary _configurations = [];
10 | private readonly List> _typeConfigurations = [];
11 |
12 | public EntityTypeBuilder Entity()
13 | {
14 | if (_configurations.TryGetValue(typeof(TEntity), out var entityTypeBuilder))
15 | return (EntityTypeBuilder)entityTypeBuilder;
16 |
17 | var newBuilder = new EntityTypeBuilder(this);
18 | _configurations[typeof(TEntity)] = newBuilder;
19 | return newBuilder;
20 | }
21 |
22 | public JsonModelBuilder Configure(Action configureAction)
23 | {
24 | _typeConfigurations.Add(configureAction);
25 | return this;
26 | }
27 |
28 | public JsonModelBuilder TrackChangedProperties()
29 | {
30 | Configure(p =>
31 | {
32 | if (!typeof(IHaveChangedProperties).IsAssignableFrom(p.Type))
33 | return;
34 |
35 | for (var i = 0; i < p.Properties.Count; i++)
36 | {
37 | var property = p.Properties[i];
38 | if (property.GetMemberInfo() is not { } mi)
39 | continue;
40 |
41 | if (mi.Name == nameof(IHaveChangedProperties.ChangedProperties))
42 | {
43 | p.Properties.RemoveAt(i);
44 | i--;
45 | continue;
46 | }
47 |
48 | if (property.Set is not { } set)
49 | continue;
50 |
51 | var realPropertyName = mi.Name;
52 |
53 | property.Set = (o, value) =>
54 | {
55 | var cp = o as IHaveChangedProperties;
56 | cp?.ChangedProperties?.Add(realPropertyName);
57 | set(o, value);
58 | };
59 | }
60 | });
61 |
62 |
63 | return this;
64 | }
65 |
66 | public JsonModelBuilder SerializeOnlyChangedProperties()
67 | {
68 | Configure(p =>
69 | {
70 | if (!typeof(IHaveChangedProperties).IsAssignableFrom(p.Type))
71 | return;
72 |
73 | for (var i = 0; i < p.Properties.Count; i++)
74 | {
75 | var property = p.Properties[i];
76 | if (property.GetMemberInfo() is not { } mi)
77 | continue;
78 |
79 | if (mi.Name == nameof(IHaveChangedProperties.ChangedProperties))
80 | {
81 | p.Properties.RemoveAt(i);
82 | i--;
83 | continue;
84 | }
85 |
86 | if (property.Get is not { })
87 | continue;
88 |
89 | var realPropertyName = mi.Name;
90 |
91 | property.ShouldSerialize = (o, value) =>
92 | {
93 | var cp = o as IHaveChangedProperties;
94 | return cp?.ChangedProperties?.Contains(realPropertyName) == true;
95 | };
96 | }
97 | });
98 |
99 |
100 | return this;
101 | }
102 |
103 |
104 | #if NET6_0_OR_GREATER
105 |
106 | public JsonModelBuilder RespectNullableReferenceType()
107 | {
108 | var nullabilityInfoContext = new NullabilityInfoContext();
109 |
110 | Configure(p =>
111 | {
112 | foreach (var prop in p.Properties)
113 | {
114 | if (prop.Set is null || !prop.PropertyType.IsClass)
115 | continue;
116 |
117 | var nullState = prop.GetMemberInfo() switch
118 | {
119 | PropertyInfo pi => nullabilityInfoContext.Create(pi).WriteState,
120 | FieldInfo fi => nullabilityInfoContext.Create(fi).WriteState,
121 | _ => NullabilityState.Unknown
122 | };
123 |
124 | if (nullState == NullabilityState.NotNull)
125 | {
126 | var set = prop.Set;
127 | var propertyName = prop.Name;
128 | prop.Set = (o, value) =>
129 | {
130 | if (value is null)
131 | throw new JsonException($"Can not set null to none nullable property {propertyName}.");
132 |
133 | set(o, value);
134 | };
135 | }
136 | }
137 | });
138 | return this;
139 | }
140 |
141 | #endif
142 | public Action Build()
143 | {
144 | var config = _configurations.ToDictionary(p => p.Key, p => p.Value.Build());
145 | var typeConfig = _typeConfigurations.ToArray();
146 | return p =>
147 | {
148 | foreach (var cfg in typeConfig)
149 | cfg(p);
150 |
151 | if (config.TryGetValue(p.Type, out var action))
152 | action(p);
153 | };
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/JsonPropertyInfoExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.Json.Serialization.Metadata;
3 |
4 | namespace SystemTextJson.FluentApi;
5 |
6 | internal static class JsonPropertyInfoExtensions
7 | {
8 | public static MemberInfo? GetMemberInfo(this JsonPropertyInfo jsonProp) =>
9 | jsonProp.AttributeProvider as MemberInfo;
10 | }
11 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/JsonSerializerOptionsExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 | using System.Text.Json.Serialization.Metadata;
4 |
5 | namespace SystemTextJson.FluentApi;
6 |
7 | public static class JsonSerializerOptionsExtensions
8 | {
9 | public static JsonSerializerOptions ConfigureDefaultTypeResolver(this JsonSerializerOptions options, Action configureAction)
10 | {
11 | var modelBuilder = new JsonModelBuilder();
12 | configureAction(modelBuilder);
13 | var action = modelBuilder.Build();
14 | options.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver();
15 | options.TypeInfoResolver = options.TypeInfoResolver.WithAddedModifier(action);
16 | return options;
17 | }
18 |
19 | public static JsonSerializerOptions ConfigureEnumValues(this JsonSerializerOptions options,
20 | IReadOnlyDictionary? mapping = null,
21 | JsonNamingPolicy? namingPolicy = null,
22 | bool allowIntegerValues = true)
23 | where TEnum : struct, Enum
24 | {
25 | var innerConverterFactory = new JsonStringEnumConverter(namingPolicy, allowIntegerValues);
26 | var innerConverter = (JsonConverter)innerConverterFactory.CreateConverter(typeof(TEnum), options)!;
27 |
28 | JsonConverter converter = mapping is null ?
29 | new CustomEnumConverter(innerConverter, options) :
30 | new CustomEnumConverter(innerConverter, options, mapping);
31 |
32 | options.Converters.Add(converter);
33 | return options;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/JsonTypeInfoResolverExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization.Metadata;
3 |
4 | namespace SystemTextJson.FluentApi;
5 |
6 | public static class JsonTypeInfoResolverExtensions
7 | {
8 | public static IJsonTypeInfoResolver ConfigureTypes(this IJsonTypeInfoResolver resolver, Action configureAction)
9 | {
10 | var modelBuilder = new JsonModelBuilder();
11 | configureAction(modelBuilder);
12 | var action = modelBuilder.Build();
13 | return resolver.WithAddedModifier(action);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/MemberPropertyBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using System.Reflection;
3 | using System.Text.Json;
4 | using System.Text.Json.Nodes;
5 | using System.Text.Json.Serialization;
6 | using System.Text.Json.Serialization.Metadata;
7 |
8 | namespace SystemTextJson.FluentApi;
9 |
10 | public class MemberPropertyBuilder(MemberInfo memberInfo, IEntityTypeBuilder entityTypeBuilder) : IMemberPropertyBuilder, IPropertyBuilder
11 | {
12 | public string Name => MemberInfo.Name;
13 | public IList> JsonPropertyInfoActions { get; } = [];
14 | public MemberInfo MemberInfo { get; } = memberInfo;
15 | IEntityTypeBuilder IPropertyBuilder.EntityTypeBuilder => entityTypeBuilder;
16 | public IEntityTypeBuilder EntityTypeBuilder => entityTypeBuilder;
17 |
18 | public MemberPropertyBuilder IsExtensionData()
19 | {
20 | if (typeof(IDictionary).IsAssignableFrom(typeof(TProperty)) ||
21 | typeof(IDictionary).IsAssignableFrom(typeof(TProperty)) ||
22 | typeof(TProperty) == typeof(JsonObject))
23 | {
24 | this.Configure(p => p.IsExtensionData = true);
25 | }
26 | else
27 | throw new InvalidOperationException($"The extension data property {typeof(TEntity)}{MemberInfo} is invalid. It must implement 'IDictionary' or 'IDictionary', or be 'JsonObject'.");
28 |
29 | return this;
30 | }
31 |
32 | public MemberPropertyBuilder IsPopulated()
33 | {
34 | this.Configure(p => p.ObjectCreationHandling = JsonObjectCreationHandling.Populate);
35 | return this;
36 | }
37 |
38 | public MemberPropertyBuilder IsRequired()
39 | {
40 | this.Configure(p => p.IsRequired = true);
41 | return this;
42 | }
43 |
44 | public MemberPropertyBuilder IsIgnored() // Add IsIgnoredIfNull/IfDefault
45 | {
46 | var mi = MemberInfo;
47 |
48 | entityTypeBuilder.Configure(p =>
49 | {
50 | for (var i = 0; i < p.Properties.Count; i++)
51 | {
52 | if (p.Properties[i].GetMemberInfo() == mi)
53 | {
54 | p.Properties.RemoveAt(i);
55 | break;
56 | }
57 | }
58 | });
59 | return this;
60 | }
61 |
62 | public MemberPropertyBuilder SerializeAsObject() =>
63 | this.Configure(p => p.CustomConverter = ObjectSerializer.Instance);
64 |
65 | Action IPropertyBuilder.Build()
66 | {
67 | var configurations = JsonPropertyInfoActions.ToArray();
68 | return p =>
69 | {
70 | foreach (var cfg in configurations)
71 | cfg(p);
72 | };
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/ObjectSerializer.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace SystemTextJson.FluentApi;
5 |
6 | internal class ObjectSerializer : JsonConverter
7 | {
8 | public static JsonConverter Instance = new ObjectSerializer();
9 |
10 | public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
11 | throw new JsonException("Can not deserialize as object.");
12 |
13 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
14 | {
15 | if (value is null)
16 | {
17 | writer.WriteNullValue();
18 | }
19 | else
20 | {
21 | var objectConverter = options.GetConverter(value.GetType());
22 | var mi = objectConverter.GetType().GetMethod(nameof(Write)) ?? throw new InvalidOperationException("JsonConverter was changed. Can't find Write method.");
23 | mi.Invoke(objectConverter, new object[] { writer, value, options });
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/PropertyBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using System.Text.Json.Serialization;
3 | using System.Text.Json.Serialization.Metadata;
4 |
5 | namespace SystemTextJson.FluentApi;
6 |
7 | public static class PropertyBuilderExtensions
8 | {
9 | public static T Configure(this T builder, Action configureAction) where T : IPropertyBuilder
10 | {
11 | builder.JsonPropertyInfoActions.Add(configureAction);
12 | return builder;
13 | }
14 |
15 | public static T HasName(this T builder, string name) where T : IPropertyBuilder =>
16 | builder.Configure(p => p.Name = name);
17 |
18 | public static T HasConverter(this T builder, JsonConverter converter) where T : IPropertyBuilder =>
19 | builder.Configure(p => p.CustomConverter = converter);
20 |
21 | public static T HasHumberHandling(this T builder, JsonNumberHandling numberHandling) where T : IPropertyBuilder =>
22 | builder.Configure(p => p.NumberHandling = numberHandling);
23 |
24 | public static T HasOrder(this T builder, int order) where T : IPropertyBuilder =>
25 | builder.Configure(p => p.Order = order);
26 |
27 | public static EntityTypeBuilder Entity(this IPropertyBuilder propertyBuilder) =>
28 | propertyBuilder.EntityTypeBuilder.ModelBuilder.Entity();
29 |
30 | public static MemberPropertyBuilder Property(this IPropertyBuilder builder, Expression> propertyExpression) =>
31 | builder.EntityTypeBuilder.Property(propertyExpression);
32 |
33 | public static VirtualPropertyBuilder VirtualProperty(this IPropertyBuilder builder, string name, Func compute) =>
34 | builder.EntityTypeBuilder.VirtualProperty(name, compute);
35 | }
36 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/SerializationHelpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.Json.Serialization;
6 | using System.Text.Json;
7 | using System.Threading.Tasks;
8 |
9 | namespace SystemTextJson.FluentApi;
10 | internal class SerializationHelpers
11 | {
12 | public static T? ReadValue(JsonConverter converter, ref Utf8JsonReader reader, JsonSerializerOptions options)
13 | {
14 | var value = default(T);
15 | if (reader.TokenType == JsonTokenType.Null && !converter.HandleNull)
16 | {
17 | if (value is not null)
18 | throw new JsonException("Expected not null value.");
19 | }
20 | else
21 | {
22 | value = converter.Read(ref reader, typeof(T), options);
23 | }
24 | reader.Read();
25 | return value;
26 | }
27 |
28 | public static void WriteValue(JsonConverter converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
29 | {
30 | if (value is null && !converter.HandleNull)
31 | writer.WriteNullValue();
32 | else
33 | converter.Write(writer, value, options);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/SystemTextJson.FluentApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1.0.1
4 | net8.0;net6.0;net7.0;netstandard2.0;net462
5 | enable
6 | enable
7 | 12
8 | true
9 | MIT
10 | True
11 | Fluent configuration library for System.Text.Json that allows developers to configure serialization uses strongly typed fluent interface and lambda expression.
12 | ilchert
13 | SystemTextJson.FluentApi is a fluent configuration library for System.Text.Json.
14 | https://github.com/Ilchert/SystemTextJson.FluentApi
15 | README.md
16 | https://github.com/Ilchert/SystemTextJson.FluentApi
17 | System.Text.Json; STJ; Serialization; JSON; Fluent Api
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/ValueTupleJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 | using static SystemTextJson.FluentApi.SerializationHelpers;
4 | namespace SystemTextJson.FluentApi;
5 | public class ValueTupleJsonConverter : JsonConverterFactory
6 | {
7 | public override bool CanConvert(Type typeToConvert)
8 | {
9 | return !typeToConvert.IsClass &&
10 | typeToConvert.IsGenericType &&
11 | typeToConvert.GetGenericTypeDefinition() is { } genericType &&
12 | (
13 | genericType == typeof(ValueTuple<>) ||
14 | genericType == typeof(ValueTuple<,>) ||
15 | genericType == typeof(ValueTuple<,,>) ||
16 | genericType == typeof(ValueTuple<,,,>) ||
17 | genericType == typeof(ValueTuple<,,,,>) ||
18 | genericType == typeof(ValueTuple<,,,,,>) ||
19 | genericType == typeof(ValueTuple<,,,,,,>) ||
20 | genericType == typeof(ValueTuple<,,,,,,,>) ||
21 | genericType == typeof(ValueTuple<,,,,,,,>)
22 | );
23 | }
24 |
25 | public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
26 | {
27 | var converterType = typeToConvert.GenericTypeArguments.Length switch
28 | {
29 | 1 => typeof(ValueTupleConverter<>),
30 | 2 => typeof(ValueTupleConverter<,>),
31 | 3 => typeof(ValueTupleConverter<,,>),
32 | 4 => typeof(ValueTupleConverter<,,,>),
33 | 5 => typeof(ValueTupleConverter<,,,,>),
34 | 6 => typeof(ValueTupleConverter<,,,,,>),
35 | 7 => typeof(ValueTupleConverter<,,,,,,>),
36 | 8 => typeof(ValueTupleConverter<,,,,,,,>),
37 | _ => throw new ArgumentOutOfRangeException(nameof(typeToConvert))
38 | };
39 |
40 | return (JsonConverter?)Activator.CreateInstance(converterType.MakeGenericType(typeToConvert.GenericTypeArguments), [options]);
41 | }
42 |
43 | private abstract class ValueTupleConverterBase : JsonConverter
44 | where TTuple : struct
45 | {
46 | public override TTuple Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
47 | {
48 | if (reader.TokenType != JsonTokenType.StartArray)
49 | throw new JsonException("Start token must be '['.");
50 |
51 | reader.Read();
52 |
53 | var result = ReadTuple(ref reader, typeToConvert, options);
54 |
55 | if (reader.TokenType != JsonTokenType.EndArray)
56 | throw new JsonException("Expected end token ']'.");
57 |
58 | return result;
59 | }
60 |
61 | protected internal abstract TTuple ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
62 |
63 | public override void Write(Utf8JsonWriter writer, TTuple value, JsonSerializerOptions options)
64 | {
65 | writer.WriteStartArray();
66 | WriteTuple(writer, value, options);
67 | writer.WriteEndArray();
68 | }
69 |
70 | protected internal abstract void WriteTuple(Utf8JsonWriter writer, TTuple value, JsonSerializerOptions options);
71 | }
72 |
73 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
74 | {
75 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
76 | protected internal override ValueTuple ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
77 | {
78 | var value1 = ReadValue(_converter1, ref reader, options);
79 | return ValueTuple.Create(value1);
80 | }
81 |
82 | protected internal override void WriteTuple(Utf8JsonWriter writer, ValueTuple value, JsonSerializerOptions options)
83 | {
84 | WriteValue(_converter1, writer, value.Item1, options);
85 | }
86 | }
87 |
88 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
89 | {
90 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
91 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2));
92 |
93 | protected internal override (T1?, T2?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
94 | {
95 | var value1 = ReadValue(_converter1, ref reader, options);
96 | var value2 = ReadValue(_converter2, ref reader, options);
97 | return (value1, value2);
98 | }
99 |
100 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?) value, JsonSerializerOptions options)
101 | {
102 | WriteValue(_converter1, writer, value.Item1, options);
103 | WriteValue(_converter2, writer, value.Item2, options);
104 | }
105 | }
106 |
107 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
108 | {
109 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
110 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2));
111 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3));
112 | protected internal override (T1?, T2?, T3?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
113 | {
114 | var value1 = ReadValue(_converter1, ref reader, options);
115 | var value2 = ReadValue(_converter2, ref reader, options);
116 | var value3 = ReadValue(_converter3, ref reader, options);
117 |
118 | return (value1, value2, value3);
119 | }
120 |
121 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?) value, JsonSerializerOptions options)
122 | {
123 | WriteValue(_converter1, writer, value.Item1, options);
124 | WriteValue(_converter2, writer, value.Item2, options);
125 | WriteValue(_converter3, writer, value.Item3, options);
126 | }
127 | }
128 |
129 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
130 | {
131 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
132 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2));
133 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3));
134 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4));
135 |
136 | protected internal override (T1?, T2?, T3?, T4?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
137 | {
138 | var value1 = ReadValue(_converter1, ref reader, options);
139 | var value2 = ReadValue(_converter2, ref reader, options);
140 | var value3 = ReadValue(_converter3, ref reader, options);
141 | var value4 = ReadValue(_converter4, ref reader, options);
142 |
143 | return (value1, value2, value3, value4);
144 | }
145 |
146 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?) value, JsonSerializerOptions options)
147 | {
148 | WriteValue(_converter1, writer, value.Item1, options);
149 | WriteValue(_converter2, writer, value.Item2, options);
150 | WriteValue(_converter3, writer, value.Item3, options);
151 | WriteValue(_converter4, writer, value.Item4, options);
152 | }
153 | }
154 |
155 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
156 | {
157 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
158 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2));
159 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3));
160 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4));
161 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5));
162 |
163 | protected internal override (T1?, T2?, T3?, T4?, T5?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
164 | {
165 | var value1 = ReadValue(_converter1, ref reader, options);
166 | var value2 = ReadValue(_converter2, ref reader, options);
167 | var value3 = ReadValue(_converter3, ref reader, options);
168 | var value4 = ReadValue(_converter4, ref reader, options);
169 | var value5 = ReadValue(_converter5, ref reader, options);
170 |
171 | return (value1, value2, value3, value4, value5);
172 | }
173 |
174 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?, T5?) value, JsonSerializerOptions options)
175 | {
176 | WriteValue(_converter1, writer, value.Item1, options);
177 | WriteValue(_converter2, writer, value.Item2, options);
178 | WriteValue(_converter3, writer, value.Item3, options);
179 | WriteValue(_converter4, writer, value.Item4, options);
180 | WriteValue(_converter5, writer, value.Item5, options);
181 | }
182 | }
183 |
184 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
185 | {
186 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
187 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2));
188 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3));
189 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4));
190 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5));
191 | private readonly JsonConverter _converter6 = (JsonConverter)options.GetConverter(typeof(T6));
192 |
193 | protected internal override (T1?, T2?, T3?, T4?, T5?, T6?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
194 | {
195 | var value1 = ReadValue(_converter1, ref reader, options);
196 | var value2 = ReadValue(_converter2, ref reader, options);
197 | var value3 = ReadValue(_converter3, ref reader, options);
198 | var value4 = ReadValue(_converter4, ref reader, options);
199 | var value5 = ReadValue(_converter5, ref reader, options);
200 | var value6 = ReadValue(_converter6, ref reader, options);
201 |
202 | return (value1, value2, value3, value4, value5, value6);
203 | }
204 |
205 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?, T5?, T6?) value, JsonSerializerOptions options)
206 | {
207 | WriteValue(_converter1, writer, value.Item1, options);
208 | WriteValue(_converter2, writer, value.Item2, options);
209 | WriteValue(_converter3, writer, value.Item3, options);
210 | WriteValue(_converter4, writer, value.Item4, options);
211 | WriteValue(_converter5, writer, value.Item5, options);
212 | WriteValue(_converter6, writer, value.Item6, options);
213 | }
214 | }
215 |
216 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
217 | {
218 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
219 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2));
220 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3));
221 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4));
222 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5));
223 | private readonly JsonConverter _converter6 = (JsonConverter)options.GetConverter(typeof(T6));
224 | private readonly JsonConverter _converter7 = (JsonConverter)options.GetConverter(typeof(T7));
225 |
226 | protected internal override (T1?, T2?, T3?, T4?, T5?, T6?, T7?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
227 | {
228 | var value1 = ReadValue(_converter1, ref reader, options);
229 | var value2 = ReadValue(_converter2, ref reader, options);
230 | var value3 = ReadValue(_converter3, ref reader, options);
231 | var value4 = ReadValue(_converter4, ref reader, options);
232 | var value5 = ReadValue(_converter5, ref reader, options);
233 | var value6 = ReadValue(_converter6, ref reader, options);
234 | var value7 = ReadValue(_converter7, ref reader, options);
235 |
236 | return (value1, value2, value3, value4, value5, value6, value7);
237 | }
238 |
239 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?, T5?, T6?, T7?) value, JsonSerializerOptions options)
240 | {
241 | WriteValue(_converter1, writer, value.Item1, options);
242 | WriteValue(_converter2, writer, value.Item2, options);
243 | WriteValue(_converter3, writer, value.Item3, options);
244 | WriteValue(_converter4, writer, value.Item4, options);
245 | WriteValue(_converter5, writer, value.Item5, options);
246 | WriteValue(_converter6, writer, value.Item6, options);
247 | WriteValue(_converter7, writer, value.Item7, options);
248 | }
249 | }
250 |
251 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase>
252 | where TRest : struct
253 | {
254 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1));
255 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2));
256 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3));
257 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4));
258 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5));
259 | private readonly JsonConverter _converter6 = (JsonConverter)options.GetConverter(typeof(T6));
260 | private readonly JsonConverter _converter7 = (JsonConverter)options.GetConverter(typeof(T7));
261 | private readonly ValueTupleConverterBase _converterRest = (ValueTupleConverterBase)options.GetConverter(typeof(TRest));
262 |
263 | protected internal override ValueTuple ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
264 | {
265 | var value1 = ReadValue(_converter1, ref reader, options);
266 | var value2 = ReadValue(_converter2, ref reader, options);
267 | var value3 = ReadValue(_converter3, ref reader, options);
268 | var value4 = ReadValue(_converter4, ref reader, options);
269 | var value5 = ReadValue(_converter5, ref reader, options);
270 | var value6 = ReadValue(_converter6, ref reader, options);
271 | var value7 = ReadValue(_converter7, ref reader, options);
272 |
273 | var restValue = _converterRest.ReadTuple(ref reader, typeof(TRest), options);
274 |
275 | return new ValueTuple(value1, value2, value3, value4, value5, value6, value7, restValue);
276 | }
277 |
278 | protected internal override void WriteTuple(Utf8JsonWriter writer, ValueTuple value, JsonSerializerOptions options)
279 | {
280 | WriteValue(_converter1, writer, value.Item1, options);
281 | WriteValue(_converter2, writer, value.Item2, options);
282 | WriteValue(_converter3, writer, value.Item3, options);
283 | WriteValue(_converter4, writer, value.Item4, options);
284 | WriteValue(_converter5, writer, value.Item5, options);
285 | WriteValue(_converter6, writer, value.Item6, options);
286 | WriteValue(_converter7, writer, value.Item7, options);
287 |
288 | _converterRest.WriteTuple(writer, value.Rest, options);
289 | }
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/src/SystemTextJson.FluentApi/VirtualPropertyBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization.Metadata;
2 |
3 | namespace SystemTextJson.FluentApi;
4 | public class VirtualPropertyBuilder(string name, IEntityTypeBuilder entityTypeBuilder) : IPropertyBuilder
5 | {
6 | public IEntityTypeBuilder EntityTypeBuilder => entityTypeBuilder;
7 |
8 | IEntityTypeBuilder IPropertyBuilder.EntityTypeBuilder => entityTypeBuilder;
9 |
10 | public string Name => name;
11 |
12 | public IList> JsonPropertyInfoActions { get; } = [];
13 |
14 | Action IPropertyBuilder.Build()
15 | {
16 | var configurations = JsonPropertyInfoActions.ToArray();
17 | return p =>
18 | {
19 | foreach (var cfg in configurations)
20 | cfg(p);
21 | };
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/SystemTextJson.FluentApi.Tests/CustomizableJsonStringEnumConverterTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Nodes;
3 | using System.Text.Json.Serialization;
4 | using Xunit;
5 |
6 | namespace SystemTextJson.FluentApi.Tests;
7 |
8 | public class CustomizableJsonStringEnumConverterTests
9 | {
10 | readonly JsonSerializerOptions _optionsWithGlobalSettings = new JsonSerializerOptions() { Converters = { new CustomizableJsonStringEnumConverter(JsonNamingPolicy.CamelCase) } };
11 |
12 |
13 | [Theory]
14 | [InlineData(A.First, "\"f\"")]
15 | [InlineData(null, "null")]
16 | [InlineData(A.Third, "\"third\"")]
17 | [InlineData((A)8, "8")]
18 | public void WriteGlobal(A? value, string json)
19 | {
20 | var actual = JsonSerializer.Serialize([value], _optionsWithGlobalSettings);
21 | Assert.True(JsonNode.DeepEquals($"""[{json}]""", actual));
22 |
23 | }
24 |
25 | [Theory]
26 | [InlineData(A.First, "\"f\"")]
27 | [InlineData(null, "null")]
28 | [InlineData(A.Third, "\"third\"")]
29 | [InlineData((A)8, "8")]
30 | public void ReadGlobal(A? value, string json)
31 | {
32 | var actual = JsonSerializer.Deserialize($"""[{json}]""", _optionsWithFluentSettings);
33 | Assert.Equal([value], actual);
34 | }
35 |
36 | readonly JsonSerializerOptions _optionsWithFluentSettings = new JsonSerializerOptions().ConfigureEnumValues(namingPolicy: JsonNamingPolicy.CamelCase);
37 |
38 | [Theory]
39 | [InlineData(A.First, "\"f\"")]
40 | [InlineData(null, "null")]
41 | [InlineData(A.Third, "\"third\"")]
42 | [InlineData((A)8, "8")]
43 | public void WriteFluentDefault(A? value, string json)
44 | {
45 | var actual = JsonSerializer.Serialize([value], _optionsWithFluentSettings);
46 | Assert.True(JsonNode.DeepEquals($"""[{json}]""", actual));
47 |
48 | }
49 |
50 | [Theory]
51 | [InlineData(A.First, "\"f\"")]
52 | [InlineData(null, "null")]
53 | [InlineData(A.Third, "\"third\"")]
54 | [InlineData((A)8, "8")]
55 | public void ReadFluentDefault(A? value, string json)
56 | {
57 | var actual = JsonSerializer.Deserialize($"""[{json}]""", _optionsWithGlobalSettings);
58 | Assert.Equal([value], actual);
59 | }
60 |
61 |
62 | readonly JsonSerializerOptions _optionsWithFluentMapping = new JsonSerializerOptions().ConfigureEnumValues(
63 | new Dictionary { { A.First, "f1" } },
64 | namingPolicy: JsonNamingPolicy.CamelCase);
65 |
66 | [Theory]
67 | [InlineData(A.First, "\"f1\"")]
68 | [InlineData(null, "null")]
69 | [InlineData(A.Third, "\"third\"")]
70 | [InlineData((A)8, "8")]
71 | public void WriteFluentMapping(A? value, string json)
72 | {
73 | var actual = JsonSerializer.Serialize([value], _optionsWithFluentMapping);
74 | Assert.True(JsonNode.DeepEquals($"""[{json}]""", actual));
75 |
76 | }
77 |
78 | [Theory]
79 | [InlineData(A.First, "\"f1\"")]
80 | [InlineData(null, "null")]
81 | [InlineData(A.Third, "\"third\"")]
82 | [InlineData((A)8, "8")]
83 | public void ReadFluentMapping(A? value, string json)
84 | {
85 | var actual = JsonSerializer.Deserialize($"""[{json}]""", _optionsWithFluentMapping);
86 | Assert.Equal([value], actual);
87 | }
88 |
89 |
90 | public enum A
91 | {
92 | None,
93 | [JsonPropertyName("f")]
94 | First = 1,
95 | Second = 2,
96 | Third = 3
97 | }
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/tests/SystemTextJson.FluentApi.Tests/EntityTypeBuilderTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization.Metadata;
2 | using System.Text.Json;
3 | using Xunit;
4 | using System.Text.Json.Serialization;
5 | using System.Reflection;
6 |
7 | namespace SystemTextJson.FluentApi.Tests;
8 |
9 | public class EntityTypeBuilderTests
10 | {
11 | private readonly JsonSerializerOptions _options;
12 | public EntityTypeBuilderTests()
13 | {
14 | _options = new JsonSerializerOptions()
15 | {
16 | TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
17 | IncludeFields = true
18 | };
19 | }
20 |
21 | [Fact]
22 | public void IsUnmappedMemberDisallowed()
23 | {
24 | _options.ConfigureDefaultTypeResolver(builder =>
25 | builder.Entity()
26 | .IsUnmappedMemberDisallowed());
27 |
28 | Assert.ThrowsAny(() => JsonSerializer.Deserialize("""{"UnmappedProperty": null}""", _options));
29 | }
30 |
31 | [Fact]
32 | public void VirtualProperty()
33 | {
34 | _options.ConfigureDefaultTypeResolver(builder =>
35 | builder.Entity()
36 | .Property(p => p.Property).IsIgnored()
37 | .Property(p => p.Field).IsIgnored()
38 | .VirtualProperty("virtualProperty", p => "computed")
39 | .Entity()
40 | .VirtualProperty("virtualProperty", p => "computed")
41 | .HasName("renamedVirtualProperty"));
42 |
43 | JsonAsserts.AssertJson(new TestClass { }, """{"renamedVirtualProperty":"computed"}""", _options);
44 | }
45 |
46 | [Fact]
47 | public void HasDerivedType()
48 | {
49 | _options.ConfigureDefaultTypeResolver(builder =>
50 | builder.Entity()
51 | .HasDerivedType(nameof(Derived1))
52 | .HasDerivedType(nameof(Derived2))
53 | .HasDerivedType(nameof(Root)));
54 |
55 | var testObject = new Root[]
56 | {
57 | new Derived1() { Derived1Property = "derived" },
58 | new Derived2() { Derived2Property = "derived2" },
59 | new Root(){ RootProperty = "root"}
60 | };
61 | JsonAsserts.AssertJsonAndObject(testObject, """
62 | [
63 | {"$type":"Derived1","Derived1Property":"derived","RootProperty":null},
64 | {"$type":"Derived2","Derived2Property":"derived2","RootProperty":null},
65 | {"$type":"Root","RootProperty":"root"}]
66 | """, _options);
67 | }
68 |
69 |
70 | [Fact]
71 | public void HasDerivedTypesFromAssembly()
72 | {
73 | _options.ConfigureDefaultTypeResolver(builder =>
74 | builder.Entity().HasDerivedTypesFromAssembly(Assembly.GetExecutingAssembly(), t => t.Name));
75 |
76 | var testObject = new Root[]
77 | {
78 | new Derived1() { Derived1Property = "derived" },
79 | new Derived2() { Derived2Property = "derived2" },
80 | new Root(){ RootProperty = "root"}
81 | };
82 | JsonAsserts.AssertJsonAndObject(testObject, """
83 | [
84 | {"$type":"Derived1","Derived1Property":"derived","RootProperty":null},
85 | {"$type":"Derived2","Derived2Property":"derived2","RootProperty":null},
86 | {"$type":"Root","RootProperty":"root"}]
87 | """, _options);
88 | }
89 |
90 |
91 | public class Root
92 | {
93 | public string? RootProperty { get; set; }
94 | }
95 |
96 | public class Derived1 : Root
97 | {
98 | public string? Derived1Property { get; set; }
99 | }
100 |
101 | public class Derived2 : Root
102 | {
103 | public string? Derived2Property { get; set; }
104 | }
105 |
106 | public class TestClass
107 | {
108 | [JsonPropertyName("Pro")]
109 | public string? Property { get; set; }
110 |
111 | public string? Field;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/tests/SystemTextJson.FluentApi.Tests/InlineArrayJsonConverterTests.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 | using System.Runtime.CompilerServices;
3 | using System.Text.Json;
4 | using System.Text.Json.Nodes;
5 |
6 | namespace SystemTextJson.FluentApi.Tests;
7 | public class InlineArrayJsonConverterTests
8 | {
9 | readonly JsonSerializerOptions _options = new() { Converters = { new InlineArrayJsonConverter() } };
10 |
11 | [Fact]
12 | public void Write()
13 | {
14 | var array = new InlineArray();
15 | array[0] = null;
16 | array[1] = 1;
17 | array[2] = -1;
18 |
19 | var actualJson = JsonSerializer.Serialize(array, _options);
20 |
21 | var isEquals = JsonNode.DeepEquals(JsonNode.Parse("[null,1,-1]"), JsonNode.Parse(actualJson));
22 | Assert.True(isEquals, "Json not equal.");
23 | }
24 |
25 | [Fact]
26 | public void WriteDto()
27 | {
28 | var array = new InlineArray();
29 | array[0] = null;
30 | array[1] = 1;
31 | array[2] = -1;
32 | var dto = new Dto() { Array = array };
33 |
34 | var actualJson = JsonSerializer.Serialize(dto, _options);
35 |
36 | var isEquals = JsonNode.DeepEquals(JsonNode.Parse("{\"Array\":[null,1,-1]}"), JsonNode.Parse(actualJson));
37 | Assert.True(isEquals, "Json not equal.");
38 | }
39 |
40 | [Fact]
41 | public void Read()
42 | {
43 | var actual = JsonSerializer.Deserialize("[null,1,-1]", _options);
44 |
45 | Assert.Null(actual[0]);
46 | Assert.Equal(actual[1], 1);
47 | Assert.Equal(actual[2], -1);
48 | }
49 |
50 |
51 | [Fact]
52 | public void ReadDto()
53 | {
54 | var actual = JsonSerializer.Deserialize("{\"Array\":[null,1,-1]}", _options);
55 |
56 | Assert.NotNull(actual);
57 | var array = Assert.NotNull(actual.Array);
58 | Assert.Null(array[0]);
59 | Assert.Equal(array[1], 1);
60 | Assert.Equal(array[2], -1);
61 | }
62 |
63 | private class Dto
64 | {
65 | public InlineArray? Array { get; set; }
66 | }
67 |
68 | [InlineArray(3)]
69 | private struct InlineArray
70 | {
71 | public int? Value;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/SystemTextJson.FluentApi.Tests/JsonAsserts.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Text.Json;
3 | using System.Text.Json.Nodes;
4 | using Xunit;
5 |
6 | namespace SystemTextJson.FluentApi.Tests;
7 |
8 | public static class JsonAsserts
9 | {
10 | public static void AssertJsonAndObject(T testObject, [StringSyntax(StringSyntaxAttribute.Json)] string expectedJson, JsonSerializerOptions options)
11 | {
12 | AssertJson(testObject, expectedJson, options);
13 | AssertObject(testObject, expectedJson, options);
14 | }
15 |
16 | public static void AssertJson