|
| 1 | +--- |
| 2 | +title: "Redis OM .NET" |
| 3 | +linkTitle: .NET |
| 4 | +description: Learn how to build with Redis Stack and .NET |
| 5 | +weight: 1 |
| 6 | +--- |
| 7 | + |
| 8 | +[Redis OM .NET](https://github.com/redis/redis-om-dotnet) is a purpose-built library for handling documents in Redis Stack. In this tutorial, we'll build a simple ASP.NET Core Web-API app for performing CRUD operations on a simple Person & Address model, and we'll accomplish all of this with Redis OM .NET. |
| 9 | + |
| 10 | +## Prerequisites |
| 11 | + |
| 12 | +* [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) |
| 13 | +* And IDE for writing .NET (Visual Studio, Rider, Visual Studio Code) |
| 14 | +* Optional: Docker Desktop for running redis-stack in docker for local testing. |
| 15 | + |
| 16 | +## Skip to the code |
| 17 | + |
| 18 | +If you want to skip this tutorial and just jump straight into code, all the source code is available in [GitHub](https://github.com/redis-developer/redis-om-dotnet-skeleton-app) |
| 19 | + |
| 20 | +## Run Redis Stack |
| 21 | + |
| 22 | +There are a variety of ways to run Redis Stack. One way is to use the docker image: |
| 23 | + |
| 24 | +``` |
| 25 | +docker run -d -p 6379:6379 -p 8001:8001 redislabs/redis-stack |
| 26 | +``` |
| 27 | + |
| 28 | +## Create the project |
| 29 | + |
| 30 | +To create the project, just run: |
| 31 | + |
| 32 | +```bash |
| 33 | +dotnet new webapi -n Redis.OM.Skeleton --no-https --kestrelHttpPort 5000 |
| 34 | +``` |
| 35 | + |
| 36 | +Then open the `Redis.OM.Skeleton.csproj` file in your IDE of choice. |
| 37 | + |
| 38 | +## Configure the app |
| 39 | + |
| 40 | +Add a `"REDIS_CONNECTION_STRING" field to your `appsettings.json` file to configure the application. Set that connection string to be the URI of your Redis instance. If using the docker command mentioned earlier, your connection string will be `redis://localhost:6379`. |
| 41 | + |
| 42 | +## Create the model |
| 43 | + |
| 44 | +Now it's time to create the `Person`/`Address` model that the app will use for storing/retrieving people. Create a new directory called `Model` and add the files `Address.cs` and `Person.cs` to it. In `Address.cs`, add the following: |
| 45 | + |
| 46 | +```csharp |
| 47 | +using Redis.OM.Modeling; |
| 48 | + |
| 49 | +namespace Redis.OM.Skeleton.Model; |
| 50 | + |
| 51 | +public class Address |
| 52 | +{ |
| 53 | + [Indexed] |
| 54 | + public int? StreetNumber { get; set; } |
| 55 | + |
| 56 | + [Indexed] |
| 57 | + public string? Unit { get; set; } |
| 58 | + |
| 59 | + [Searchable] |
| 60 | + public string? StreetName { get; set; } |
| 61 | + |
| 62 | + [Indexed] |
| 63 | + public string? City { get; set; } |
| 64 | + |
| 65 | + [Indexed] |
| 66 | + public string? State { get; set; } |
| 67 | + |
| 68 | + [Indexed] |
| 69 | + public string? PostalCode { get; set; } |
| 70 | + |
| 71 | + [Indexed] |
| 72 | + public string? Country { get; set; } |
| 73 | + |
| 74 | + [Indexed] |
| 75 | + public GeoLoc Location { get; set; } |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +Here, you'll notice that except `StreetName`, marked as `Searchable`, all the fields are decorated with the `Indexed` attribute. These attributes (`Searchable` and `Indexed`) tell Redis OM that you want to be able to use those fields in queries when querying your documents in Redis Stack. `Address` will not be a Document itself, so the top-level class is not decorated with anything; instead, the `Address` model will be embedded in our `Person` model. |
| 80 | + |
| 81 | +To that end, add the following to `Person.cs` |
| 82 | + |
| 83 | +```csharp |
| 84 | +using Redis.OM.Modeling; |
| 85 | + |
| 86 | +namespace Redis.OM.Skeleton.Model; |
| 87 | + |
| 88 | +[Document(StorageType = StorageType.Json, Prefixes = new []{"Person"})] |
| 89 | +public class Person |
| 90 | +{ |
| 91 | + [RedisIdField] [Indexed]public string? Id { get; set; } |
| 92 | + |
| 93 | + [Indexed] public string? FirstName { get; set; } |
| 94 | + |
| 95 | + [Indexed] public string? LastName { get; set; } |
| 96 | + |
| 97 | + [Indexed] public int Age { get; set; } |
| 98 | + |
| 99 | + [Searchable] public string? PersonalStatement { get; set; } |
| 100 | + |
| 101 | + [Indexed] public string[] Skills { get; set; } = Array.Empty<string>(); |
| 102 | + |
| 103 | + [Indexed(CascadeDepth = 1)] Address? Address { get; set; } |
| 104 | + |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +There are a few things to take note of here: |
| 109 | + |
| 110 | +1. `[Document(StorageType = StorageType.Json, Prefixes = new []{"Person"})]` Indicates that the data type that Redis OM will use to store the document in Redis is JSON and that the prefix for the keys for the Person class will be `Person`. |
| 111 | + |
| 112 | +2. `[Indexed(CascadeDepth = 1)] Address? Address { get; set; }` is one of two ways you can index an embedded object with Redis OM. This way instructs the index to cascade to the objects in the object graph, `CascadeDepth` of 1 means that it will traverse just one level, indexing the object as if it were building the index from scratch. The other method uses the `JsonPath` property of the individual indexed fields you want to search for. This more surgical approach limits the size of the index. |
| 113 | + |
| 114 | +3. the `Id` property is marked as a `RedisIdField`. This denotes the field as one that will be used to generate the document's key name when it's stored in Redis. |
| 115 | + |
| 116 | +## Create the Index |
| 117 | + |
| 118 | +With the model built, the next step is to create the index in Redis. The most correct way to manage this is to spin the index creation out into a Hosted Service, which will run which the app spins up. Create a' HostedServices' directory and add `IndexCreationService.cs` to that. In that file, add the following, which will create the index on startup. |
| 119 | + |
| 120 | +```csharp |
| 121 | +using Redis.OM.Skeleton.Model; |
| 122 | + |
| 123 | +namespace Redis.OM.Skeleton.HostedServices; |
| 124 | + |
| 125 | +public class IndexCreationService : IHostedService |
| 126 | +{ |
| 127 | + private readonly RedisConnectionProvider _provider; |
| 128 | + public IndexCreationService(RedisConnectionProvider provider) |
| 129 | + { |
| 130 | + _provider = provider; |
| 131 | + } |
| 132 | + |
| 133 | + public async Task StartAsync(CancellationToken cancellationToken) |
| 134 | + { |
| 135 | + await _provider.Connection.CreateIndexAsync(typeof(Person)); |
| 136 | + } |
| 137 | + |
| 138 | + public Task StopAsync(CancellationToken cancellationToken) |
| 139 | + { |
| 140 | + return Task.CompletedTask; |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +## Inject the RedisConnectionProvider |
| 146 | + |
| 147 | +Redis OM uses the `RedisConnectionProvider` class to handle connections to Redis and provides the classes you can use to interact with Redis. To use it, simply inject an instance of the RedisConnectionProvider into your app. In your `Program.cs` file, add: |
| 148 | + |
| 149 | +```csharp |
| 150 | +builder.Services.AddSingleton(new RedisConnectionProvider(builder.Configuration["REDIS_CONNECTION_STRING"])); |
| 151 | +``` |
| 152 | + |
| 153 | +This will pull your connection string out of the config and initialize the provider. The provider will now be available in your controllers/services to use. |
| 154 | + |
| 155 | +## Create the PeopleController |
| 156 | + |
| 157 | +The final puzzle piece is to write the actual API controller for our People API. In the `controllers` directory, add the file `PeopleController.cs`, the skeleton of the `PeopleController`class will be: |
| 158 | + |
| 159 | +```csharp |
| 160 | +using Microsoft.AspNetCore.Mvc; |
| 161 | +using Redis.OM.Searching; |
| 162 | +using Redis.OM.Skeleton.Model; |
| 163 | + |
| 164 | +namespace Redis.OM.Skeleton.Controllers; |
| 165 | + |
| 166 | +[ApiController] |
| 167 | +[Route("[controller]")] |
| 168 | +public class PeopleController : ControllerBase |
| 169 | +{ |
| 170 | + |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +### Inject the RedisConnectionProvider |
| 175 | + |
| 176 | +To interact with Redis, inject the RedisConnectionProvider. During this dependency injection, pull out a `RedisCollection<Person>` instance, which will allow a fluent interface for querying documents in Redis. |
| 177 | + |
| 178 | +```csharp |
| 179 | +private readonly RedisCollection<Person> _people; |
| 180 | +private readonly RedisConnectionProvider _provider; |
| 181 | +public PeopleController(RedisConnectionProvider provider) |
| 182 | +{ |
| 183 | + _provider = provider; |
| 184 | + _people = (RedisCollection<Person>)provider.RedisCollection<Person>(); |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +### Add route for creating a Person |
| 189 | + |
| 190 | +The first route to add to the API is a POST request for creating a person, using the `RedisCollection`, it's as simple as calling `InsertAsync`, passing in the person object: |
| 191 | + |
| 192 | + |
| 193 | +```csharp |
| 194 | +[HttpPost] |
| 195 | +public async Task<Person> AddPerson([FromBody] Person person) |
| 196 | +{ |
| 197 | + await _people.InsertAsync(person); |
| 198 | + return person; |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +### Add route to filter by age |
| 203 | + |
| 204 | +The first filter route to add to the API will let the user filter by a minimum and maximum age. Using the LINQ interface available to the `RedisCollection`, this is a simple operation: |
| 205 | + |
| 206 | +```csharp |
| 207 | +[HttpGet("filterAge")] |
| 208 | +public IList<Person> FilterByAge([FromQuery] int minAge, [FromQuery] int maxAge) |
| 209 | +{ |
| 210 | + return _people.Where(x => x.Age >= minAge && x.Age <= maxAge).ToList(); |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +### Filter by GeoLocation |
| 215 | + |
| 216 | +Redis OM has a `GeoLoc` data structure, an instance of which is indexed by the `Address` model, with the `RedisCollection`, it's possible to find all objects with a radius of particular position using the `GeoFilter` method along with the field you want to filter: |
| 217 | + |
| 218 | + |
| 219 | +```csharp |
| 220 | +[HttpGet("filterGeo")] |
| 221 | +public IList<Person> FilterByGeo([FromQuery] double lon, [FromQuery] double lat, [FromQuery] double radius, [FromQuery] string unit) |
| 222 | +{ |
| 223 | + return _people.GeoFilter(x => x.Address!.Location, lon, lat, radius, Enum.Parse<GeoLocDistanceUnit>(unit)).ToList(); |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +### Filter by exact string |
| 228 | + |
| 229 | +When a string property in your model is marked as `Indexed`, e.g. `FirstName` and `LastName`, Redis OM can perform exact text matches against them. For example, the following two routes filter by `PostalCode` and name demonstrate exact string matches. |
| 230 | + |
| 231 | +```csharp |
| 232 | +[HttpGet("filterName")] |
| 233 | +public IList<Person> FilterByName([FromQuery] string firstName, [FromQuery] string lastName) |
| 234 | +{ |
| 235 | + return _people.Where(x => x.FirstName == firstName && x.LastName == lastName).ToList(); |
| 236 | +} |
| 237 | + |
| 238 | +[HttpGet("postalCode")] |
| 239 | +public IList<Person> FilterByPostalCode([FromQuery] string postalCode) |
| 240 | +{ |
| 241 | + return _people.Where(x => x.Address!.PostalCode == postalCode).ToList(); |
| 242 | +} |
| 243 | +``` |
| 244 | + |
| 245 | +### Filter with a full-text search |
| 246 | + |
| 247 | +When a property in the model is marked as `Searchable`, like `StreetAddress` and `PersonalStatement`, you can perform a full-text search, see the filters for the `PersonalStatement` and `StreetAddress`: |
| 248 | + |
| 249 | + |
| 250 | +```csharp |
| 251 | +[HttpGet("fullText")] |
| 252 | +public IList<Person> FilterByPersonalStatement([FromQuery] string text){ |
| 253 | + return _people.Where(x => x.PersonalStatement == text).ToList(); |
| 254 | +} |
| 255 | + |
| 256 | +[HttpGet("streetName")] |
| 257 | +public IList<Person> FilterByStreetName([FromQuery] string streetName) |
| 258 | +{ |
| 259 | + return _people.Where(x => x.Address!.StreetName == streetName).ToList(); |
| 260 | +} |
| 261 | +``` |
| 262 | + |
| 263 | +### Filter by array membership |
| 264 | + |
| 265 | +When a string array or list is marked as `Indexed`, Redis OM can filter all the records containing a given string using the `Contains` method of the array or list. For example, our `Person` model has a list of skills you can query by adding the following route. |
| 266 | + |
| 267 | +```csharp |
| 268 | +[HttpGet("skill")] |
| 269 | +public IList<Person> FilterBySkill([FromQuery] string skill) |
| 270 | +{ |
| 271 | + return _people.Where(x => x.Skills.Contains(skill)).ToList(); |
| 272 | +} |
| 273 | +``` |
| 274 | + |
| 275 | +### Updating a person |
| 276 | + |
| 277 | +Updating a document in Redis Stack with Redis OM can be done by first materializing the person object, making your desired changes, and then calling `Save` on the collection. The collection is responsible for keeping track of updates made to entities materialized in it; therefore, it will track and apply any updates you make in it. For example, add the following route to update the age of a Person given their Id: |
| 278 | + |
| 279 | + |
| 280 | +```csharp |
| 281 | +[HttpPatch("updateAge/{id}")] |
| 282 | +public IActionResult UpdateAge([FromRoute] string id, [FromBody] int newAge) |
| 283 | +{ |
| 284 | + foreach (var person in _people.Where(x => x.Id == id)) |
| 285 | + { |
| 286 | + person.Age = newAge; |
| 287 | + } |
| 288 | + _people.Save(); |
| 289 | + return Accepted(); |
| 290 | +} |
| 291 | +``` |
| 292 | + |
| 293 | +### Delete a person |
| 294 | + |
| 295 | +Deleting a document from Redis can be done with `Unlink`. All that's needed is to call Unlink, passing in the key name. Given an id, we can reconstruct the key name using the prefix and the id: |
| 296 | + |
| 297 | + |
| 298 | +```csharp |
| 299 | +[HttpDelete("{id}")] |
| 300 | +public IActionResult DeletePerson([FromRoute] string id) |
| 301 | +{ |
| 302 | + _provider.Connection.Unlink($"Person:{id}"); |
| 303 | + return NoContent(); |
| 304 | +} |
| 305 | +``` |
| 306 | + |
| 307 | +## Run the app |
| 308 | + |
| 309 | +All that's left to do now is to run the app and test it. You can do so by running `dotnet run`, the app is now exposed on port 5000, and there should be a swagger UI that you can use to play with the API at http://localhost:5000/swagger. There's a couple of scripts, along with some data files, to insert some people into Redis using the API in the [GitHub repo](https://github.com/redis-developer/redis-om-dotnet-skeleton-app/tree/main/data) |
| 310 | + |
| 311 | +## Viewing data in with Redis Insight |
| 312 | + |
| 313 | +You can either install the Redis Insight GUI or use the Redis Insight GUI running on http://localhost:8001/. |
| 314 | + |
| 315 | +You can view the data by following these steps: |
| 316 | + |
| 317 | +1. Accept the EULA |
| 318 | + |
| 319 | + |
| 320 | + |
| 321 | +2. Click the Add Redis Database button |
| 322 | + |
| 323 | + |
| 324 | + |
| 325 | +3. Enter your hostname and port name for your redis server. If you are using the docker image, this is `localhost` and `6379` and give your database an alias |
| 326 | + |
| 327 | + |
| 328 | + |
| 329 | +4. Click `Add Redis Database.` |
| 330 | + |
| 331 | +## Resources |
| 332 | + |
| 333 | +* The source code for this tutorial can be found in [GitHub](https://github.com/redis-developer/redis-om-dotnet-skeleton-app). |
| 334 | +* To learn more about Redis OM you can check out the the guide on [Redis Developer](https://developer.redis.com/develop/dotnet/redis-om-dotnet/connecting-to-redis) |
0 commit comments