Skip to content

Commit

Permalink
More docs
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveDunn committed Dec 10, 2023
1 parent 90af870 commit 016fe48
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 15 deletions.
3 changes: 3 additions & 0 deletions docs/site/Writerside/hi.tree
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<toc-element topic="NormalizationTutorial.md"/>
<toc-element topic="Specifying-pre-set-values.md"/>
<toc-element topic="Serialization.md"/>
<toc-element topic="Using-with-JSON.md"/>
<toc-element topic="Working-with-databases.md"/>
</toc-element>
<toc-element topic="how-to-guides.topic">
<toc-element topic="create-and-use-value-objects.md"/>
Expand All @@ -28,6 +30,7 @@
<toc-element topic="Testing.md"/>
<toc-element topic="Overriding-methods.md"/>
<toc-element topic="Value-Objects-in-EFCore.md"/>
<toc-element topic="Use-in-Swagger.md"/>
</toc-element>
<toc-element topic="Reference.md">
<toc-element topic="Configuration.md"/>
Expand Down
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.
4 changes: 4 additions & 0 deletions docs/site/Writerside/redirection-rules.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@
<description>Created after removal of "How to create Value Objects" from Vogen</description>
<accepts>Basics.html</accepts>
</rule>
<rule id="6f7ba5bc">
<description>Created after removal of "Working with protobuf" from Vogen</description>
<accepts>Working-with-protobuf.html</accepts>
</rule>
</rules>
82 changes: 82 additions & 0 deletions docs/site/Writerside/topics/Use-in-Swagger.md
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>
16 changes: 16 additions & 0 deletions docs/site/Writerside/topics/Using-with-JSON.md
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.
30 changes: 30 additions & 0 deletions docs/site/Writerside/topics/Working-with-databases.md
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.
30 changes: 30 additions & 0 deletions docs/site/Writerside/topics/reference/Integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,33 @@ public enum Conversions
```

The default, as specified above in the `Defaults` property, is `TypeConverter` and `SystemTextJson`.


-- todo: merge this in:

There are other converters/serializer for:

* Newtonsoft.Json (NSJ)
* Dapper
* EFCore
* [LINQ to DB](https://github.com/linq2db/linq2db)
* protobuf-net (see the FAQ section below)

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.
102 changes: 87 additions & 15 deletions docs/site/Writerside/topics/tutorials/Serialization.md
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>

0 comments on commit 016fe48

Please sign in to comment.