-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
252 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
# Use in Swagger | ||
|
||
You can use Vogen types as parameters in ASP.NET Core Web API endpoints. | ||
That is because the .NET runtime will notice the `TypeConverter` that is generated by default, and use that to try | ||
to convert the underlying type (e.g. `string` or `int`) to the value object. | ||
|
||
However, Swagger treats value object fields as JSON, e.g. given this method: | ||
|
||
```C# | ||
[HttpGet("/WeatherForecast/{cityName}")] | ||
public IEnumerable<WeatherForecast> Get(CityName cityName) | ||
{ | ||
... | ||
} | ||
``` | ||
|
||
... You can use it (e.g. at `http://localhost:5053/WeatherForecast/London`) without having to do anything else, | ||
but Swagger will show the field as JSON instead of the underlying type: | ||
|
||
<img border-effect="rounded" alt="swagger-json-parameter.png" src="swagger-json-parameter.png"/> | ||
|
||
To fix this, add this to your project: | ||
|
||
```C# | ||
public class VogenSchemaFilter : ISchemaFilter | ||
{ | ||
public void Apply(OpenApiSchema schema, SchemaFilterContext context) | ||
{ | ||
if (context.Type.GetCustomAttribute<ValueObjectAttribute>() is not { } attribute) | ||
return; | ||
|
||
// Since we don't hold the actual type, we ca only use the generic attribute | ||
var type = attribute.GetType(); | ||
|
||
if (!type.IsGenericType || type.GenericTypeArguments.Length != 1) | ||
{ | ||
return; | ||
} | ||
|
||
var schemaValueObject = context.SchemaGenerator.GenerateSchema( | ||
type.GenericTypeArguments[0], | ||
context.SchemaRepository, | ||
context.MemberInfo, context.ParameterInfo); | ||
|
||
CopyPublicProperties(schemaValueObject, schema); | ||
} | ||
|
||
private static void CopyPublicProperties<T>(T oldObject, T newObject) where T : class | ||
{ | ||
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; | ||
|
||
if (ReferenceEquals(oldObject, newObject)) | ||
{ | ||
return; | ||
} | ||
|
||
var type = typeof(T); | ||
var propertyList = type.GetProperties(flags); | ||
if (propertyList.Length <= 0) return; | ||
|
||
foreach (var newObjProp in propertyList) | ||
{ | ||
var oldProp = type.GetProperty(newObjProp.Name, flags)!; | ||
if (!oldProp.CanRead || !newObjProp.CanWrite) continue; | ||
|
||
var value = oldProp.GetValue(oldObject); | ||
newObjProp.SetValue(newObject, value); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
... and then, when you register Swagger, tell it to use this new filter: | ||
|
||
```C# | ||
builder.Services.AddSwaggerGen(opt => opt.SchemaFilter<VogenSchemaFilter>()); | ||
``` | ||
Many thanks to [Vitalii Mikhailov](https://github.com/Aragas) for this contribution. | ||
|
||
<note> | ||
Although this is currently a manual step, it is intended for this to be put into a NuGet package at some point. | ||
</note> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Working with JSON | ||
|
||
<note> | ||
This documentation is currency incomplete and is being improved. | ||
</note> | ||
|
||
As well as System.Text.Json (STJ), Vogen also generates the code to work with Newtonsoft.Json (NSJ) | ||
|
||
They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ: | ||
|
||
```c# | ||
[ValueObject<float>(conversions: Conversions.NewtonsoftJson | Conversions.SystemTextJson)] | ||
public readonly partial struct Celsius { } | ||
``` | ||
|
||
See the examples folder for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Working with databases | ||
|
||
<note> | ||
This documentation is currency incomplete and is being improved. | ||
</note> | ||
|
||
There are other converters/serializer for: | ||
|
||
* Dapper | ||
* EFCore | ||
* [LINQ to DB](https://github.com/linq2db/linq2db) | ||
|
||
They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ: | ||
|
||
```c# | ||
[ValueObject(conversions: Conversions.NewtonsoftJson | Conversions.SystemTextJson, underlyingType: typeof(float))] | ||
public readonly partial struct Celsius { } | ||
``` | ||
|
||
If you don't want any conversions, then specify `Conversions.None`. | ||
|
||
If you want your own conversion, then again specify none, and implement them yourself, just like any other type. But be aware that even serializers will get the same compilation errors for `new` and `default` when trying to create VOs. | ||
|
||
If you want to use Dapper, remember to register it—something like this: | ||
|
||
```c# | ||
SqlMapper.AddTypeHandler(new Customer.DapperTypeHandler()); | ||
``` | ||
|
||
See the examples folder for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,100 @@ | ||
# Serializing and persisting | ||
# Working with ASP.NET Core | ||
|
||
By default, each VO is decorated with a `TypeConverter` and `System.Text.Json` (STJ) serializer. There are other converters/serializer for: | ||
In this tutorial, we'll create a type and see how to serialize and deserialize it. | ||
|
||
* Newtonsoft.Json (NSJ) | ||
* Dapper | ||
* EFCore | ||
* [LINQ to DB](https://github.com/linq2db/linq2db) | ||
* protobuf-net (see the FAQ section below) | ||
First, create a value object: | ||
|
||
They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ: | ||
```C# | ||
[ValueObject<int>] | ||
public partial struct CustomerId { } | ||
``` | ||
|
||
In the `ValueObject`, we don't specify any additional configuration other than the underlying primitive that is | ||
being wrapped. | ||
We'll see shortly how to add this configuration, but for now, we'll look at the default configuration | ||
and what it allows us to do. | ||
|
||
By default, each value object generates a `TypeConverter` and `System.Text.Json` (STJ) serializer. | ||
We can see this from the following snippet of generated code: | ||
|
||
```C# | ||
[global::System.Text.Json.Serialization.JsonConverter(typeof(CustomerIdSystemTextJsonConverter))] | ||
[global::System.ComponentModel.TypeConverter(typeof(CustomerIdTypeConverter))] | ||
public partial struct CustomerId | ||
{ | ||
} | ||
``` | ||
|
||
'Type Converters' are used by the .NET runtime in things like ASP.NET Core Web Applications, grids, and WPF bindings. | ||
Let's see how that works in a web API project. | ||
|
||
Create a new ASP.NET Core Web API targeting .NET 7.0. In the project, look at the endpoint named `GetWeatherForecast` in | ||
the `WeatherForecastController` type. It looks like this: | ||
|
||
```C# | ||
[HttpGet(Name = "GetWeatherForecast")] | ||
public IEnumerable<WeatherForecast> Get() | ||
{ | ||
... | ||
} | ||
``` | ||
|
||
To demonstrate how we can use Value Objects, we'll add a `CityName` value object and use it as part of the request. | ||
|
||
Create a `CityName` type: | ||
|
||
```c# | ||
[ValueObject(conversions: Conversions.NewtonsoftJson | Conversions.SystemTextJson, underlyingType: typeof(float))] | ||
public readonly partial struct Celsius { } | ||
[ValueObject<string>] | ||
public partial struct CityName { } | ||
``` | ||
|
||
If you don't want any conversions, then specify `Conversions.None`. | ||
... now, add this as a parameter to the endpoint: | ||
```C# | ||
[HttpGet("/WeatherForecast/{cityName}")] | ||
public IEnumerable<WeatherForecast> Get(CityName cityName) | ||
{ | ||
} | ||
``` | ||
|
||
Add a `CityName` property to the `WeatherForecast` type: | ||
|
||
```C# | ||
public class WeatherForecast | ||
{ | ||
public CityName CityName { get; set; } | ||
|
||
public DateOnly Date { get; set; } | ||
|
||
If you want your own conversion, then again specify none, and implement them yourself, just like any other type. But be aware that even serializers will get the same compilation errors for `new` and `default` when trying to create VOs. | ||
... | ||
``` | ||
|
||
If you want to use Dapper, remember to register it—something like this: | ||
Now, change the code in the endpoint to populate the new `CityName` property. | ||
We're keeping things simple and just returning what we were provided: | ||
|
||
```c# | ||
SqlMapper.AddTypeHandler(new Customer.DapperTypeHandler()); | ||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast | ||
{ | ||
+ CityName = cityName, | ||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), | ||
TemperatureC = Random.Shared.Next(-20, 55), | ||
Summary = Summaries[Random.Shared.Next(Summaries.Length)] | ||
}) | ||
.ToArray(); | ||
``` | ||
|
||
See the examples folder for more information. | ||
You can now access that via the URL and treat the value object as a string, | ||
e.g. `http://localhost:5053/WeatherForecast/London` | ||
You should see the City returned, something like this: | ||
|
||
<img border-effect="rounded" alt="return-from-web-api.png" src="return-from-web-api.png"/> | ||
|
||
|
||
<note> | ||
You'll need to replace for 5053 to whatever is specified in the project just created, which can be | ||
found in launchSettings.json. | ||
|
||
Also, you'll see that Swagger treats cityName as a JSON field. This can be changed as detailed in [this How-to article](Use-in-Swagger.md). | ||
|
||
</note> | ||
|